Website Contact Scraper – Emails, Phones, Socials
Pricing
from $0.50 / 1,000 page reads
Website Contact Scraper – Emails, Phones, Socials
Scrape a business's contact details off its website: every email address and validated phone number, social and messaging profiles, postal address and contact form — one row per site. It reads the page you give plus the few pages likeliest to carry contacts, because a home page rarely does.
Pricing
from $0.50 / 1,000 page reads
Rating
0.0
(0)
Developer
Simple Actors
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
3 days ago
Last modified
Categories
Share
Give it a website. Get back how to reach the business behind it — email addresses, phone numbers, social accounts, the postal address and the contact form — as one row per website. Built for lead generation, CRM enrichment, sales prospecting and list building from nothing but a column of domains.
It does not just read the page you give it. Contact details are usually not on a home page: across the first ten real sites this was built against, eight published no email address on their home page and none published a phone number. So it reads the entry page, scores that site's own links for how likely each is to lead to contact details, and reads the best few as well — the contact page, the imprint, the about or team page, whichever that site actually has.
Two things it is careful about:
- A wrong phone number is worse than no phone number. Numbers are validated against the real numbering plan of their country, and are read from the page's visible text and its links, never from its raw markup. On one real home page a phone-shaped pattern over the markup returned about seventy matches — CSS aspect ratios, image IDs, timestamps — and not one phone number.
- A site that could not be read never comes back looking like a site with no
contact details. It arrives as an error row with no
emailsfield at all. See Failures.
Features
- Every email on the site, not just the first — each with the page it was
found on and how it was found (
mailto, JSON-LD, Cloudflare-obfuscated, plain text…). - Phone numbers validated against the real numbering plan — returned in E.164, with national formatting, line type and country.
- Finds the contact page for itself — scores a site's own links and reads the contact, imprint, about or team page, whichever that site actually has.
- Social and messaging profiles separated — accounts to follow apart from WhatsApp, Telegram, Messenger and the rest, with share buttons rejected.
- Postal address only when it really is one — structured data first, and from prose only with a house number, street and postal code together.
- Contact-form URL for businesses that publish a form instead of an address.
- Named contact points where the site groups them by person or department.
- The real logo, not the favicon — the image the site shows at the top of
its page, at the largest size it offers, with
logoSourcesaying whether you got that or fell back to an icon. - Company identity too — name and description.
- Bare hosts accepted — paste a column straight out of a spreadsheet.
- Cost capped per site —
maxPagesPerSiteis a hard ceiling on what any one website can cost. - JavaScript rendered only when needed.
- Unreadable is never confused with empty — an error row carries no
emailskey at all. - Runs stay green. No site problem and no input problem fails a run.
Input
{"urls": ["https://www.bungalowny.com/"],"maxPagesPerSite": 5}
A bare host works too — paste a column straight out of a spreadsheet:
{ "urls": ["bungalowny.com", "dishoom.com", "apify.com"] }
| Field | Type | Default | What it does |
|---|---|---|---|
startUrls / urls | array | — | The websites. Both are read; either one is enough. urls also accepts bare hosts. |
maxPagesPerSite | integer | 5 | How many pages of each site may be read — and your ceiling on what each website costs, since charging is per page. 1 reads only the page you gave, which is the cheapest and finds roughly half as many email addresses. Above about 6 there is little left to find. |
renderJs | string | auto | auto opens a browser only when the served HTML held nothing at all. always renders everything. never refuses the browser. |
countryHint | string | — | A two-letter code such as GB. Only used to read phone numbers written without a country code. |
proxy | object | Apify datacenter | Switch to residential if sites answer with challenge pages. |
Output
One row per website:
{"type": "website","url": "https://www.bungalowny.com/","finalUrl": "https://www.bungalowny.com/","domain": "bungalowny.com","name": "Bungalow","description": "Modern Indian Flagship restaurant by Michelin Star Chef Vikas Khanna","logo": "https://images.squarespace-cdn.com/content/v1/65983d57438d713cfc579485/290d5546/Bungalow_FullLogo_Mint.png?format=1500w","logoSource": "header","email": "info@bungalowny.com","phone": null,"emails": [{ "email": "info@bungalowny.com", "source": "mailto", "foundOn": "https://www.bungalowny.com/" },{ "email": "info@gupshupnyc.com", "source": "mailto", "foundOn": "https://www.bungalowny.com/" }],"phones": [],"contacts": [{ "name": null, "role": null, "email": "info@bungalowny.com", "phone": null, "source": "dom", "sourceUrl": "https://www.bungalowny.com/" }],"social": {"instagram": "https://instagram.com/bungalowny","facebook": "https://www.facebook.com/p/Bungalow-NY-61558260149142"},"messaging": {},"socialProfiles": [{ "platform": "instagram", "url": "https://instagram.com/bungalowny", "handle": "bungalowny" },{ "platform": "facebook", "url": "https://www.facebook.com/p/Bungalow-NY-61558260149142", "handle": "Bungalow-NY-61558260149142" }],"address": { "street": null, "city": null, "region": null, "postalCode": null, "country": null, "raw": "24 First Avenue, New York, NY 10009", "source": "text" },"contactFormUrl": null,"phoneCountry": "US","rendered": false,"pagesRead": [{ "url": "https://www.bungalowny.com/", "statusCode": 200, "rendered": false, "role": "entry" },{ "url": "https://www.bungalowny.com/hours-and-location", "statusCode": 200, "rendered": false, "role": "linked" }],"pagesReadCount": 5,"retrievedAt": "2026-08-28T00:31:12.904Z"}
The four fields most people use
email— the single best address to write to. Not the first one found: an address on the site's own domain beats a Gmail one, a general address (info@,hello@,contact@) beats a personal one, one published as amailto:link beats one mentioned in prose, and anoreply@address goes to the bottom.phone— the single best number, in E.164 (+12125551234), ready to dial. Where a site declares in its structured data which number is for customer service, that is the one.social.<platform>— one URL per platform:social.facebook,social.instagram,social.twitter,social.linkedin,social.youtube,social.tiktok, and a couple of dozen more. Only platforms actually found are present, so{}means no social accounts were linked.contacts[0]— always carries the best email and the best phone together, socontacts[0].emailandcontacts[0].phoneare the two fields to read if you read nothing else. Later entries are the groupings the site itself made — a department in its structured data, or an email and a phone sitting in the same block of the page.
The logo, and how to tell a logo from a favicon
Most tools report a site's apple-touch-icon as its logo. That is a favicon:
square, often 32 pixels, and on a Wix or Squarespace site frequently the site
builder's own default icon rather than the business's mark at all.
This Actor reads the logo off the page first — the image at the top named
logo, usually inside the link back to the home page — and takes the largest
version the markup offers. Across seventeen real sites measured, eleven had a
logo image to find and every one was correct; the other six draw their logo as
inline SVG, which has no address to hand you.
logoSource says which route answered, and it is the field to filter on:
logoSource | What you got |
|---|---|
header | The logo the page displays. The one to trust. |
jsonld | The logo the site declares in its structured data. Usually right, sometimes a stray photograph. |
og:logo | The same, from a meta tag. |
apple-touch-icon, icon | A favicon — square, small, but the business's own. |
og:image | The site's share picture, which may be a photograph. |
null | The site offers nothing. |
Nothing is fetched to decide this, so the field costs you nothing.
What counts as a social account
A link to a platform is not an account on it. These are all excluded on purpose, and each was found on a real page:
| Left out | Why |
|---|---|
tiktok.com/discover/<business name> | TikTok's search results, not the business |
youtube.com/watch?v=… | one video, not a channel |
facebook.com/sharer/…, twitter.com/intent/tweet, linkedin.com/shareArticle | share buttons |
instagram.com/p/… | a post |
linkedin.com/, facebook.com/ | the platform's own front page |
facebook.com/p/Name-123/ and facebook.com/profile.php?id=… are kept — that
is how a page with no vanity URL is linked. Tracking parameters are stripped, so
one account never appears as several.
Messaging is separate. messaging.whatsapp, messaging.telegram,
messaging.messenger, messaging.skype, messaging.viber and messaging.line
are ways to start a conversation now rather than accounts to follow. A wa.me
link also contributes its digits to phones, because it is a phone number the
site published.
Which pages get read
The entry page always. Then that site's own links, ranked — an imprint outranks even a contact page, because German law requires that page to carry a postal address, a phone number and an email, which makes it the richest page on a European site. Then contact, support, about, team, locations.
Both the link's address and the words that were clickable are scored, because
neither alone is reliable: /about is a 404 on most sites, whose about pages
live at /who-we-are or /our-story, while one site's contact page is at
/say-hello/ and only the label "Contact us" says so.
If a site links nothing that looks like a contact page, conventional addresses
(/contact, /contact-us, and /impressum or /kontakt on a German domain)
are tried instead. pagesRead on every row says exactly which pages were read
and whether each was linked or guessed.
How to use
From Apify Console
- Open the Actor and click Try for free / Start.
- Paste your domains into Websites (plain list) — bare hosts like
dishoom.comwork, so a spreadsheet column goes straight in. - Leave Pages to read per website at 5. That is your cost ceiling per site, and it averages 3.9 pages in practice because guessed addresses that turn out not to exist are never charged. Set it to 1 for the cheapest possible run, which finds roughly half as many email addresses.
- If your list is mostly one country and its sites write phone numbers without a country code, set Country for phone numbers to that two-letter code.
- Click Start, then open the Dataset tab and export as JSON, CSV or Excel.
Skip rows with no emails key — those are sites that could not be read, not
sites without contact details.
From the API
curl -s "https://api.apify.com/v2/acts/simple.actors~website-contacts/run-sync-get-dataset-items?token=$APIFY_TOKEN" \-H 'Content-Type: application/json' \-d '{"urls": ["bungalowny.com", "dishoom.com"], "maxPagesPerSite": 5}'
Enriching a list of domains, with the JavaScript client:
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: process.env.APIFY_TOKEN });const run = await client.actor('simple.actors/website-contacts').call({urls: ['bungalowny.com', 'dishoom.com', 'apify.com'],maxPagesPerSite: 5,countryHint: 'GB',});const { items } = await client.dataset(run.defaultDatasetId).listItems();for (const row of items) {if (!('emails' in row)) continue; // unread, not "no contacts" — key on presenceconsole.log(row.domain, row.email, row.phone, row.pagesReadCount);}
Use cases
- Lead generation and list building — turn a column of domains into emails, phone numbers and contact-form URLs ready for outreach.
- CRM and sales data enrichment — fill in missing contact routes, postal addresses and social handles against accounts you already hold.
- Sales prospecting from a market list — feed in the domains from a directory, search or competitor scrape and get the way in for each.
- Social profile discovery — collect a business's real accounts, with share buttons and platform boilerplate already rejected.
- Company data and directory building — name, description, logo, postal address and contact details in one row per site.
- Deliverability and contact hygiene checks — see which of your accounts still publish a working address, and which publish none at all.
Usage notes
Simple, transparent pay-per-result pricing — no subscriptions.
What it costs
Charged per page read, plus one small fee per run. Nothing else.
| a run | one run fee, charged once |
| each page read | one page event |
| a page that turns out not to exist | free |
| a website that could not be read at all | free |
| a run where nothing could be read | free — no run fee either |
You control the bill directly with Pages to read per website. At the default of 5 a site averages 3.9 pages, because guessed addresses that turn out not to exist are never charged. Set it to 1 and a website costs exactly one page — the cheapest run there is, and it finds roughly half as many email addresses.
Charging per page rather than per website is deliberate: a site that links its
contact page from its home page costs two reads, and one that hides its address
behind a five-page hunt costs five. You pay for the second only when it happens.
Every row carries pagesReadCount, which is exactly what that row was charged.
Failures
The run always succeeds. A site that refuses us, a host that does not resolve, an address that is not a URL — each becomes a row, and the run's status message counts them. Only a bug in this Actor fails a run.
So the failure channel is the row shape, and there is one rule:
A site that publishes no contact details has
emails: []. A site that could not be read has noemailsfield at all.
If your pipeline acts on emptiness — retiring a lead, marking a record checked —
key off the presence of emails, never off the run status.
{"type": "error","url": "https://www.katzsdelicatessen.com/","statusCode": 405,"error": "challenge","errorDescription": "https://www.katzsdelicatessen.com/ answered with a challenge or a refusal (\"Human Verification\") rather than the page.","retrievedAt": "2026-08-28T00:31:12.904Z"}
error | What happened |
|---|---|
bad_input | The address was not usable |
unreachable | The host did not answer |
http_error | It answered with a status that is not a page |
challenge | It answered with a bot check or a refusal — try residential proxy |
not_found | There is no page at that address |
render_failed | Its scripts had to be run and the browser could not |
read_failed | Anything else that stopped the read |
Notes on accuracy
Phone numbers written without a country code need a country to be read at
all — 020 7946 0958 is a real number in the UK and not a number anywhere
else. The country is taken from countryHint if you give one, then the site's
own structured data, then its domain ending, falling back to US. Every row
reports which was used as phoneCountry. If a site's numbers look wrong, that
is the first field to check, and countryHint is the fix.
A postal address is only reported when it is really one. Structured data
first. Read out of the page's own words it must carry a house number, a street
and a postal code, in that order, in a short span of text — a sentence that
merely mentions a street is not an address, and neither is a delivery radius or
a set of opening hours. address.source says which of the four routes found it.
Empty is an answer. Plenty of well-known businesses publish no email address anywhere on their site, and this Actor will tell you so rather than inventing one.
FAQ
Is scraping contact details from websites legal? This Actor reads publicly published pages — the contact details a business puts on its own site for people to use. Using them is a separate question: check the platform's ToS, and check your local rules on personal data and marketing consent (GDPR, CAN-SPAM and their equivalents) before emailing or calling anything you collect. A named person's work email is personal data in many jurisdictions.
Does it guess or verify email addresses?
Neither — it reports what the site publishes and nothing else. No pattern like
firstname@domain is ever invented, and no mailbox is pinged to check it
exists. An empty emails array means the site genuinely published none.
Why did I get no phone number when the site clearly shows one?
Most often the number is written without a country code, so it cannot be
validated — 020 7946 0958 is a real number in the UK and nowhere else. Set
countryHint to the right two-letter code. Every row reports which country was
used, as phoneCountry.
How many pages does it read per website?
Up to maxPagesPerSite, which defaults to 5 and averages 3.9 in practice.
It reads the page you gave plus the highest-scoring links on it — the contact
page, imprint, about or team page. Set it to 1 to read only the page you gave.
Can it crawl a whole website? No, deliberately. It reads the few pages likeliest to carry contact details, not the whole site — that is what keeps the cost per website bounded and predictable.
How do I tell "no contact details" from "could not read the site"?
By whether the row has an emails key. A site that was read has one, even if it
is empty. A site that could not be read has no emails field at all and carries
error instead. Key your pipeline on the presence of the field, not its length.
Does it find social media profiles?
Yes, and it separates two kinds: social holds accounts to follow, messaging
holds WhatsApp, Telegram, Messenger and similar. Share buttons and platform
boilerplate are rejected rather than returned as the business's own accounts.
Why did my run succeed when a site failed? By design — a site that cannot be read is an answer about that site, not a fault in the Actor, so it becomes a row and the run stays green. Only a bug in the Actor fails a run.