Website Contact Scraper – Emails, Phones, Socials avatar

Website Contact Scraper – Emails, Phones, Socials

Pricing

from $0.50 / 1,000 page reads

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

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

Simple Actors

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

3 days ago

Last modified

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 emails field 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 logoSource saying 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 sitemaxPagesPerSite is 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 emails key 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"] }
FieldTypeDefaultWhat it does
startUrls / urlsarrayThe websites. Both are read; either one is enough. urls also accepts bare hosts.
maxPagesPerSiteinteger5How 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.
renderJsstringautoauto opens a browser only when the served HTML held nothing at all. always renders everything. never refuses the browser.
countryHintstringA two-letter code such as GB. Only used to read phone numbers written without a country code.
proxyobjectApify datacenterSwitch 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 a mailto: link beats one mentioned in prose, and a noreply@ 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, so contacts[0].email and contacts[0].phone are 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:

logoSourceWhat you got
headerThe logo the page displays. The one to trust.
jsonldThe logo the site declares in its structured data. Usually right, sometimes a stray photograph.
og:logoThe same, from a meta tag.
apple-touch-icon, iconA favicon — square, small, but the business's own.
og:imageThe site's share picture, which may be a photograph.
nullThe 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 outWhy
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/shareArticleshare 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

  1. Open the Actor and click Try for free / Start.
  2. Paste your domains into Websites (plain list) — bare hosts like dishoom.com work, so a spreadsheet column goes straight in.
  3. 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.
  4. 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.
  5. 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 presence
console.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 runone run fee, charged once
each page readone page event
a page that turns out not to existfree
a website that could not be read at allfree
a run where nothing could be readfree — 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 no emails field 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"
}
errorWhat happened
bad_inputThe address was not usable
unreachableThe host did not answer
http_errorIt answered with a status that is not a page
challengeIt answered with a bot check or a refusal — try residential proxy
not_foundThere is no page at that address
render_failedIts scripts had to be run and the browser could not
read_failedAnything else that stopped the read

Notes on accuracy

Phone numbers written without a country code need a country to be read at all020 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.