Company Contact Details & Website Email Finder avatar

Company Contact Details & Website Email Finder

Pricing

$2.00 / 1,000 website with an emails

Go to Apify Store
Company Contact Details & Website Email Finder

Company Contact Details & Website Email Finder

Contact information scraper for company websites: find email addresses, phone numbers, WhatsApp numbers and social media links (LinkedIn, Facebook, Instagram, X) from URLs or domains. B2B lead generation, one row per site. You pay $0.002 per site with an email. Ranked bestEmail for your CRM.

Pricing

$2.00 / 1,000 website with an emails

Rating

0.0

(0)

Developer

Data Gleaner

Data Gleaner

Maintained by Community

Actor stats

0

Bookmarked

51

Total users

40

Monthly active users

13 minutes ago

Last modified

Share

Company Contact Details & Website Email Finder: finds emails other scrapers miss

Paste company websites, get one row per site with the best email to write to. It finds addresses that plain contact scrapers return empty, and you pay $0.002 only for sites where an email is found; every other site is free.

Why this one

  • Finds emails other scrapers miss. On 22 small-company sites where our previous build found 1 email, the current build found 20 (Apify run, datacenter IPs). On 883 sites it went from 433 to 483 with an email, and none lost an address. Those 22 were picked because they had failed, so 20 of 22 is not a population rate; the 883-site result (+5.7 points) is.
  • How: when the normal crawl finds nothing, it also reads the contact, about and legal pages listed in sitemap.xml, WordPress's public REST pages, and the site's own JavaScript files. It decodes Cloudflare email protection, tag-split addresses (info<span>@</span>example.com), reversed text, and atob(), String.fromCharCode() and \u0040 strings.
  • Compatible with the input and output of the most popular contact scraper. Send startUrls and set outputFormat: "compatible" and your existing downstream mapping keeps working (see Switching from another contact scraper below).
  • Pay per result, $0.002 per site with an email ($2 per 1,000). Sites with no email, blocked sites and sites with only a phone or form cost nothing, so a run costs sites with an email x $0.002.
  • One row per website, bestEmail ready for a CRM column. Every email has a kind (general, sales, support, press, hr, legal) sorted best lead first; placeholder and no-reply-only addresses are dropped.
  • No setup. Paste websites and click Start. Plain HTTP by default: no browser, proxies or crawl rules to configure. A browser pass and a residential proxy retry exist as opt-in switches (both off by default).
  • Also returned: phones (E.164), the site's own social profiles, contact forms and postal addresses, each with the page it came from.
  • Blocked sites get a second try. A site that answers the plain request with HTTP 403, 429 or 503, or with a bot-check page, is retried once through Apify Proxy with a browser's request headers. Sites that answer normally never touch the proxy. See the cost section for what this adds.

Minimal input

{
"websites": ["appier.com", "stripe.com", "https://www.sakura.ad.jp"]
}

Only websites is needed. Full URLs and bare domains both work.

Sample output row (shortened)

{
"website": "https://www.appier.com",
"domain": "appier.com",
"status": "ok",
"companyName": "Appier",
"bestEmail": "contactus-hk@appier.com",
"emailList": ["contactus-hk@appier.com"],
"phoneList": ["+886287802800"],
"emails": [{"value": "contactus-hk@appier.com", "kind": "general", "foundOn": "https://www.appier.com/en/contact"}]
}

Not every site publishes an address: in our test, 64% of a mixed set of sites returned an email (see Measured results below). Sites that block non-browser clients or publish only a form are returned as rows without a bestEmail, and are not billed.

What it does

For each website it fetches the home page, then the pages most likely to hold contact details: contact, about, impressum / legal notice, team, company profile and footer links, including Japanese (お問い合わせ, 会社概要, 特定商取引法), Chinese (聯絡我們, 關於我們), German (Kontakt, Impressum), French, Spanish and Italian equivalents. It stays on the same site (a subdomain only when a link clearly names a contact page, such as help.example.com/contact), follows redirects (http to https, www or not), respects robots.txt and does at most maxPagesPerSite pages per site (default 8).

Extracted:

  • Emails: mailto: links, plain text, obfuscated forms (name [at] domain [dot] com, name(at)domain.com), Cloudflare email protection (also inside scripts), JSON-LD (email and contactPoint), microdata, HTML-entity and split-tag forms (info<span>@</span>example.com), reversed text, and atob(), String.fromCharCode() or \u0040 encoded strings in scripts. When the normal crawl finds no email of the site's own, it makes a few extra requests outside maxPagesPerSite: the contact, about and legal pages listed in sitemap.xml, WordPress's public REST pages (/wp-json/wp/v2/pages?search=contact), and the site's own script files. Addresses on the site's own domain (or a brand-named domain, or webmail such as Gmail) go in emails; addresses on unrelated domains (partners, quoted customers, demo data) are kept apart in otherEmails and are not counted as the site's contacts or billed. On gnu.org, for example, 38 other-domain addresses landed in otherEmails and none in emails.
  • Best email first: emails is sorted best lead first (company inbox such as info@ or hello@, sales, a named person, then support, press, HR, legal and privacy) and each entry has a kind. bestEmail is the one to write to first. No-reply senders and form placeholders (username@...) are never returned as contacts, and a site whose only address is a no-reply sender is not billed. Addresses that a page draws from script data (Next.js and similar JSON) are read too.
  • Phone numbers: tel: links, text and JSON-LD, validated and normalized to E.164 with Google's libphonenumber. Numbers written after a label (Tel, Phone, Call, Mobile, 電話) are accepted even without spaces or dashes and are listed first. Numbers labelled Fax are skipped, and so are dates, prices, order numbers and anything libphonenumber does not call a valid number. Local numbers are read using the site's country, inferred from the domain, page language and structured data (or your defaultCountry).
  • Social profiles on 14 platforms: LinkedIn, X (Twitter), Facebook, Instagram, YouTube, TikTok, GitHub, Pinterest, Threads, Telegram, WhatsApp, Discord, Reddit and Snapchat. socials holds the site's own accounts (linked from its header, footer or nav, or matching its brand); other people's profiles it merely links (testimonials, team, press) go in mentionedSocials. Share buttons and post links are ignored.
  • Contact forms: pages with a message form, plus embedded Google Forms, Typeform, HubSpot, Marketo, Pardot, Jotform, Calendly and similar, including forms a script draws after the page loads (recognized from the embed code, since pages are not rendered).
  • Addresses: schema.org PostalAddress in JSON-LD and microdata (itemprop), <address> elements, a labelled Address: line, and plain text in footers and contact pages where a street line is followed by a postal code (US ZIP, UK postcode, Canadian code, German, French and other 4-5 digit codes, Japanese 〒). A line needs both a street and a postal code to count, so dates, prices and ID numbers are not read as addresses. Addresses found in text have "source": "text" and a single formatted line; structured ones carry street, locality and postal code fields. The same address found twice is returned once.
  • Company name and description from JSON-LD, Open Graph and meta tags.

Use cases

Lead generation and sales prospecting from a company list, enriching a CRM, building supplier or partner directories, outreach research, checking that a company's published contact details are current.

Input

{
"websites": ["https://www.sakura.ad.jp", "appier.com", "https://stripe.com"],
"maxPagesPerSite": 8,
"respectRobotsTxt": true,
"defaultCountry": "",
"maxConcurrency": 10,
"maxConcurrencyPerDomain": 2,
"requestTimeoutSecs": 20
}

websites takes full URLs or bare domains. extractPeople (default false) adds a people list per site, see People on team pages below. maxPagesPerSite is 1 to 30 (default 8). maxConcurrency (sites in parallel, 1 to 50, default 10), maxConcurrencyPerDomain (1 to 5, default 2), requestTimeoutSecs (3 to 120, default 20) and proxyConfiguration (default: no proxy) tune speed and politeness. renderFallback and residentialRetry (both default false) are described in the cost section. With an empty websites list the Actor runs a built-in example on apify.com.

Crawl depth and URL patterns

  • maxDepth (0 to 10, default 1) is how many links away from the start URL the crawl may go. 0 reads the start page only, with no links followed and no sitemap lookup. 1 (the default) reads the start page and the contact, about, legal and team pages it links to. 2 or more also follows contact-like links found on those pages, always within maxPagesPerSite.
  • includeUrlPatterns and excludeUrlPatterns (lists of text, empty by default) decide which further pages are followed. The start URL is always read, exclude wins over include, and patterns also apply to the pages found through the sitemap and WordPress lookups. A pattern is matched against the full URL, ignoring case: * matches any characters and ? one character; a pattern without them matches when the URL contains it (/blog/); a pattern with them must match the whole URL, so start it with * to match anywhere (*/team/*) or with / to match the path only (/en/*). Start with re: for a regular expression (re:/(contact|about)). Objects {"glob": "..."} and {"regexp": "..."} work too.
{"websites": ["acme.example"], "maxDepth": 2, "maxPagesPerSite": 10, "includeUrlPatterns": ["/about*", "re:/(contact|team)"], "excludeUrlPatterns": ["/blog/", "*/careers/*"]}

Switching from another contact scraper

Paste your existing input and keep your downstream mapping:

  • startUrls works as an alias of websites: a list of {"url": "https://example.com"} objects. It is merged with websites and datasetId, and duplicates are removed.
  • maxDepth and maxRequestsPerStartUrl are read as they are in other contact scrapers (maxRequestsPerStartUrl is the same setting as maxPagesPerSite). One difference: when maxDepth is empty, other scrapers follow links to any depth, while here it means 1 (the start page and the pages it links to), which is what finds contacts at the lowest cost. Set maxDepth to a larger number to go deeper. includeUrlGlobs and excludeUrlGlobs are accepted as other names of the two URL pattern lists. Keys this Actor does not use (proxyConfig, sameDomain, mergeContacts, useBrowser) are ignored; only the same site is ever crawled, and every site is one merged row.
  • outputFormat: "compatible" adds the field names other contact scrapers use to every row: url, originalStartUrl, domain, depth, referrerUrl, scrapedUrls, emails and phones (plain string lists), phonesUncertain, and one list per platform (linkedIns, twitters, instagrams, facebooks, youtubes, tiktoks, pinterests, discords, snapchats, threads, telegrams, reddits, whatsapps). All our own fields stay; only emails and phones change from detailed objects to plain strings, and the detailed objects are kept as emailDetails and phoneDetails. Pricing is unchanged: one row per site, billed only when an email is found.
{"startUrls": [{"url": "https://www.gymshark.com"}, {"url": "https://apify.com"}], "outputFormat": "compatible"}

Have company names, not websites?

Put the names in companyNames, one per line (add a location after a comma for same-named companies, e.g. Bosch, Germany; companyCountry sets a country for all of them). The Actor first runs Company Domain Finder on your account to find each company's official website, then scans those websites exactly like the ones in websites.

{"companyNames": ["Stripe", "Notion", "Basecamp"]}
  • What it bills: Company Domain Finder charges its own $0.0008 per website found (nothing when no website is found). This Actor then charges its usual $0.002 per site with an email, and nothing for the rest. The two charges appear as separate runs in your account.
  • Output: each row has companyName (the name you gave) and the resolved website. A name with no website found, or only a low-confidence guess, gets one free row with failureReason: "no website found for company name".
  • Names are merged with websites, startUrls and datasetId, and a website that appears twice is scanned once. If the lookup fails or runs out of time, the affected names come back as free rows with a failureReason and the run still succeeds.

Have a Google Maps search, not a list of websites?

Put the searches in searchQueries, one per line, for example dentists in Denver, CO; maxPlacesPerQuery sets how many places to take from each search (default 20, up to 200). The Actor first runs our Google Maps Leads Actor on your account to find the places, then scans each place's website exactly like the ones in websites.

{"searchQueries": ["dentists in Denver, CO", "plumbers in Austin, TX"], "maxPlacesPerQuery": 20}
  • What it bills: Google Maps Leads charges its own per-place price for every place it returns (its website-contact and decision-maker options are switched off in this sub-run, so you are not billed twice for contacts). This Actor then charges its usual $0.002 per site with an email, and nothing for the rest.
  • Output: each row has the website plus placeName, placeAddress, placeCategory, placePhone, placeRating, placeUrl (the Google Maps link) and searchQuery. A place with no website gets one free row with failureReason: "no_website_on_maps" and the place fields.
  • Places are merged with websites, startUrls, companyNames and datasetId, and a website that appears twice is scanned once. If the search fails or runs out of time, the status message says so and the run still scans the other inputs.

Run it after Google Maps Scraper or any scraper

Give the Actor a datasetId and it reads the website of every item in that dataset, so it works as the next step after a scraper that returns places, companies or shops, for example Google Maps Scraper (compass/crawler-google-places) or any other dataset that has a website field.

As an integration (no code): open the source Actor's Integrations tab, add this Actor, and set its input to:

{"datasetId": "{{resource.defaultDatasetId}}"}

Every time the source run succeeds, this Actor starts on its results. It also works as an integration without setting any input: in the source Actor's Integrations tab choose Add integration and pick this Actor, and the dataset is passed automatically.

From the API or Python:

from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("datagleaner/website-contact-details-scraper").call(
run_input={"datasetId": "DATASET_ID_OF_THE_OTHER_RUN"}
)
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
print(item.get("sourceTitle"), item["website"], item["bestEmail"])

How it reads the dataset:

  • The website is the first present value of website, url, domain, site, homepage, link, companyWebsite, web. Set datasetUrlField to name a different field; it is tried first.
  • Items with an empty or non-http value are skipped, and a website that appears twice (or is also in websites) is scanned once.
  • Each output row has sourceTitle, the item's title or name, so you can see which place the row belongs to.
  • maxDatasetItems (default 1000) caps how many items are read. The status message says how many websites came from the dataset and how many items had none.
  • Billing does not change: you pay only for sites where an email is found.

Output

One item per input website.

{
"website": "https://www.appier.com",
"domain": "appier.com",
"finalUrl": "https://www.appier.com/en/",
"status": "ok",
"error": null,
"companyName": "Appier",
"description": "Appier is a software-as-a-service company ...",
"bestEmail": "contactus-hk@appier.com",
"emailList": ["contactus-hk@appier.com"],
"phoneList": ["+886287802800"],
"linkedin": ["https://www.linkedin.com/company/appier"],
"emails": [{"value": "contactus-hk@appier.com", "foundOn": "https://www.appier.com/en/contact", "kind": "general"}],
"otherEmails": [],
"phones": [{"value": "+886287802800", "raw": "+886-2-8780-2800", "country": "TW", "foundOn": "https://www.appier.com/en/contact"}],
"socials": {
"linkedin": [{"url": "https://www.linkedin.com/company/appier", "foundOn": "https://www.appier.com/en/"}],
"twitter": [], "facebook": [], "instagram": [], "youtube": [], "tiktok": [], "github": [], "pinterest": [], "threads": [],
"telegram": [], "whatsapp": [], "discord": [], "reddit": [], "snapchat": []
},
"mentionedSocials": {"linkedin": [], "twitter": []},
"contactForms": [{"url": "https://www.appier.com/en/contact", "foundOn": "https://www.appier.com/en/contact"}],
"addresses": [{"streetAddress": "7 Xinyi Rd", "addressLocality": "Taipei", "addressRegion": null, "postalCode": "110", "addressCountry": "TW", "formatted": "7 Xinyi Rd, Taipei, 110, TW", "foundOn": "https://www.appier.com/en/"}],
"pagesCrawled": 8,
"pageUrls": ["https://www.appier.com/en/"],
"scrapedAt": "2026-10-07T15:40:00+00:00"
}

Every value has a foundOn page. For CSV, Sheets, Clay or Zapier use the flat columns: emailList and phoneList (plain strings), one list per platform (linkedin, twitter, ... snapchat, URLs only), and domain (host without www.) as a join key for CRM matching. mentionedSocials has the same 14 platform keys as socials (shortened above).

status is ok (at least one contact found), noContacts, unreachable (network error or HTTP error such as 403/429), blockedByRobots, invalidUrl or error.

failureReason says why a row has no bestEmail, and is null when it has one. Rows with a reason are free; only a site with an email is billed. The values:

failureReasonMeaning
no_contact_foundThe site was read and shows no contact of its own.
no_email_foundPhones, forms, social profiles or addresses were found, but no usable email (only no-reply addresses, or only domains that cannot receive mail).
blocked_403, rate_limited_429The site refused the request, also after the proxy retry.
blocked_challengeThe site served a bot-check page instead of its content.
http_<code>Any other HTTP error, for example http_404 or http_500.
timeoutThe site did not answer in time, or used up its 90 second budget.
dns_errorThe domain does not resolve.
connection_errorThe connection was refused or dropped.
robots_disallowedThe site's robots.txt disallows the home page (see respectRobotsTxt).
invalid_urlThe input is not a website address.
crawl_errorAn unexpected error while reading the site.

The run's final status message counts the sites per reason.

People on team pages

Set "extractPeople": true (default false) and each site row also gets a people list: name, job title, seniority (c-level, vp, director, manager, other), a department guess, an email only when the site prints one next to that person, and the sourceUrl it came from. Up to 25 people per site, most senior first.

It reads the company's own site only: team, about, leadership and people pages found through the home page links, the sitemap and common paths (up to 12 extra pages per site, on top of maxPagesPerSite), from structured data, team cards, name and title pairs, and sentences such as "Jane Doe, our CEO" or "founded by A and B". No LinkedIn, no third-party databases, no guessed emails. Testimonials, privacy text and blog bylines are skipped.

Pricing: people are a free add-on for now. The only charge is still the website-with-email event, so turning extractPeople on costs nothing extra (it adds a little run time). With it off, the output is exactly as before.

On a test of 20 company sites (SaaS, agencies and small businesses, run on Apify in October 2026), 9 sites (45%) had at least one person with a title, 107 people in all, and 28 of 30 hand-checked people had the right name and title. Many small companies publish no team page, or only first names, and then people is an empty list.

{
"name": "Tope Awotona",
"title": "Founder, Chief Executive Officer",
"seniority": "c-level",
"department": "executive",
"email": null,
"sourceUrl": "https://calendly.com/about"
}

How to extract emails and phone numbers from a list of websites

  1. Open the Actor and paste your company websites into websites (full URLs or bare domains such as appier.com).
  2. Leave maxPagesPerSite at 8 for most sites. Raise it (up to 30) for large sites with deep contact or team pages.
  3. Click Start. Each site produces one item as soon as it is done.
  4. Open the Output tab and export as CSV, Excel or JSON, or fetch the dataset through the API.

How much does it cost to scrape contact details?

Pay per event: US$2.00 per 1,000 websites with an email (event website-with-email, US$0.002 each). You are charged once per website where at least one email was found on the site's own domain (addresses on other domains and other people's profiles do not count). Sites with only a phone, social profile or contact form, and sites that are unreachable or have nothing, are returned free.

Worked example: 1,000 websites, of which about 60% have an email, costs 600 x $0.002 = $1.20. Platform usage is small because the Actor makes plain HTTP requests only.

Proxy retry cost. retryBlockedWithProxy is on by default. Only sites that block the plain request use Apify Proxy (datacenter), and only for that site's pages, so the proxy traffic of a run is a small share of its sites. The Actor's own price does not change. The proxy traffic is billed by Apify to your account as platform usage: in our test the 8 retried sites added about US$0.006 of platform usage in total (mostly extra run time, since a blocked site is tried again and some stay blocked), roughly US$0.0006 to US$0.0008 per retried site, which is about US$0.07 per 1,000 sites if one in ten blocks. A site that is retried and still refused is not billed by this Actor. Set retryBlockedWithProxy to false to switch it off, or set your own proxyConfiguration to send every request through it instead.

Browser fallback cost. renderFallback is off by default and the default run is unchanged. When on, a site that still has no email after the normal crawl is opened in a headless browser: its home page and its best contact page, 2 pages at most, reading the final page and the data the page loaded. A run with the fallback on starts with 2048 MB of memory by default (1024 MB otherwise); if you set the memory yourself, give it 2048 MB or more. The Actor's own price does not change and only sites with an email are billed. The cost is platform usage and time. On 60 sites that had returned no email in earlier runs, a run with the fallback took 467 s and US$0.049 of platform usage against 280 s and US$0.030 without it, and found 3 more emails, 5% of those sites (one was on a page the browser saw but a plain request did not; some of the gain can also be a plain request that failed by chance in the other run). On a mixed set of 30 normal sites it added about 30 s and US$0.006 and found no extra email, because the plain crawl had already found them. The image that contains the browser starts slower the first time a machine pulls it (a first empty run took 17 s, later ones 3 s). Switch it on when a list is full of small-business and site-builder pages and the plain crawl leaves many sites empty.

Residential retry cost. residentialRetry is off by default. When on, a blocked site is retried through Apify's residential proxy instead of the datacenter proxy. Residential traffic is billed by Apify at a per-gigabyte rate: the 6 large sites that refused the datacenter proxy used about 2 MB and US$0.017 in one run, and 1 of the 6 (a German price-comparison site) then returned its email. The other 5 still refused. Expect a recovery for about one blocked site in six at roughly US$0.003 per blocked site tried, and nothing for sites with the strictest bot protection.

Per-page contact scrapers charge per page crawled (about $2 per 1,000 pages at the time of writing, with a 20-page default), which comes to roughly $20 to $40 per 1,000 sites. Here you pay $2 per 1,000 sites with an email, and every other site is free. Check the other Actor's current pricing before comparing.

Measured results

Tested on the Apify platform (datacenter IPs) on 25 real sites in October 2026: 5 SaaS, 3 agencies, 5 German or EU sites with an impressum, 5 small blogs and restaurants, 4 sites that rely on contact forms or JavaScript, plus 3 hard-to-reach ones.

  • Email found on 16 of 25 sites (64%), phone on 9 (36%), an own social profile on 17 (68%), a contact form on 10 (40%), at least one contact of any kind on 19 (76%).
  • Of the 9 sites without an email: 2 returned HTTP 403 or 429 (jwz.org, personio.com), 1 refused our crawler on the home page (bluebottlecoffee.com), 3 publish a form or phone but no address (mailchimp.com, hubspot.com, joespizzanyc.com), and 3 show no contact address on the pages we read.
  • Junk: 0 image-file names, sentry or wixpress addresses; 1 placeholder (username@mailbox.org) in the first version, 0 after the placeholder filter.
  • 49 emails in total, 2.0 per site that has any; the first one is the best-ranked kind.

A different 25 sites will give a different rate. Sites that block non-browser clients were the main loss in this test; since the proxy retry was added, blocked sites get one more try (see Retry results below), but a site that blocks Apify's proxy addresses too is still returned as unreachable. The Actor does not solve captchas.

Retry results (Apify run, datacenter IPs, October 2026). On a set of 30 sites (22 small-company sites plus 8 that block plain requests): sites with an email 20 before, 21 after (one site that returned HTTP 403 now returns its email, another now loads and has no email). Of the 8 blocked sites, 1 was recovered and 1 loaded but had no contact details; the other 6 (large retail sites with strict bot protection) still refuse Apify Proxy and stay unreachable. No site that worked before got worse. Phone and address parsing, same 30 sites: sites with an address 2 before, 4 after; on 12 further larger-company sites, sites with an address 5 before, 10 after, sites with a phone 9 before and after (3 fax numbers that used to be listed as phones were dropped, each confirmed as labelled Fax on the page).

Deliverability check. Every email's domain is checked in DNS (MX record, falling back to an A/AAAA record per RFC 5321) and gets a deliverable flag: true if the domain can receive mail, false if it does not exist, has a null MX, or has no MX and no address record, and null if DNS was inconclusive. bestEmail prefers deliverable addresses and bestEmailDeliverable carries the flag into CSV exports. A site whose only emails are on domains that cannot receive mail is returned but not billed. The check uses DNS only and never connects to a mail server, so it tells you the domain takes mail, not that a given mailbox exists. On 17 live sites all 29 emails found were on domains that take mail; the check earns its keep on lapsed or mistyped domains.

Limits

  • No JavaScript rendering by default: details injected only by scripts are not found unless you switch on renderFallback, which reads 2 pages in a browser for sites with no email. Sites that block non-browser clients (HTTP 403/429) get one retry through Apify Proxy; if that is refused too, the site is returned as unreachable.
  • Phone and address text parsing reads the page text as a person would. Addresses in a map image, a script-drawn widget or an unusual postal format can be missed, and a block of text that looks like a street plus a postal code can occasionally be a false match; check the source and foundOn fields.
  • Emails and phones shown as images are not read.
  • Up to 30 pages per site; default 8. A site gets no new request after 90 seconds; a server that takes 20 seconds or more per page is returned with what was found by then, so one slow site cannot hold the whole run open.
  • Only the same site is crawled (plus a subdomain when a link clearly names a contact page); the Actor does not follow links to other sites.

Use with Python

# pip install apify-client
from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("datagleaner/website-contact-details-scraper").call(
run_input={"websites": ["https://www.appier.com", "stripe.com"], "maxPagesPerSite": 5}
)
for item in client.dataset(run.default_dataset_id).iterate_items():
print(item["website"], item["status"], [e["value"] for e in item["emails"]])

Use with JavaScript / Node.js

// npm install apify-client
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('datagleaner/website-contact-details-scraper').call({
websites: ['https://www.appier.com', 'stripe.com'],
maxPagesPerSite: 5,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const item of items) console.log(item.website, item.status, item.emails.map((e) => e.value));

Use it from n8n, Make, Zapier or an AI agent

Actor ID: datagleaner/website-contact-details-scraper

Minimal input:

{"websites": ["https://www.appier.com", "stripe.com"]}

Each tool below runs this Actor with your own Apify API token.

  • n8n: add the Apify node (@apify/n8n-nodes-apify). On n8n Cloud you install it from the community node registry. Choose Run an Actor and get dataset, set Actor to datagleaner/website-contact-details-scraper and paste the input above.

  • Make: use the Apify app's Run an Actor module, then Get Dataset Items to read the results. Watch Actor Runs can trigger a scenario when a run finishes.

  • Zapier: use the Apify action Run Actor, then the search Fetch dataset items. The trigger Finished Actor run starts a Zap when a run ends.

  • AI agents (MCP): connect to https://mcp.apify.com?tools=datagleaner/website-contact-details-scraper. In Claude Code:

    claude mcp add --transport http apify "https://mcp.apify.com?tools=datagleaner/website-contact-details-scraper"
    

    Then run /mcp to sign in to Apify in your browser. Other clients can sign in with OAuth or send the header Authorization: Bearer YOUR_APIFY_TOKEN. Clients that run local MCP servers can use Apify's package (@apify/actors-mcp-server, run with npx -y and APIFY_TOKEN set) instead. Then ask the agent in plain words, for example:

    Find the contact email, phone number and LinkedIn page for each of these companies: appier.com, stripe.com, sakura.ad.jp. Put the results in a table with the page each one was found on.

  • LangChain (Python):

# pip install langchain-apify, then set APIFY_TOKEN in your environment
import json
from langchain_apify import ApifyActorsTool
tool = ApifyActorsTool("datagleaner/website-contact-details-scraper")
result = tool.invoke({"run_input": json.loads('{"websites": ["https://www.appier.com", "stripe.com"]}')})

FAQ

How do I extract emails from a website? Put the website in websites. The Actor reads the home page and the contact, about, legal-notice and team pages, and returns every email it finds with the page it came from, including obfuscated forms such as name [at] domain [dot] com.

Can I scrape emails from a list of websites in bulk? Yes. Give it thousands of domains in one run; it works through them in parallel (maxConcurrency sites at once, at most maxConcurrencyPerDomain requests per site), and each site becomes one row.

Does it also scrape phone numbers and social media links? Yes. Phones are validated and normalized to E.164 with libphonenumber, and social links cover 14 platforms (LinkedIn, X, Facebook, Instagram, YouTube, TikTok, GitHub, Pinterest, Threads, Telegram, WhatsApp, Discord, Reddit, Snapchat).

Is there a free way to try it? Sites with no email are not charged, and a run of a few sites costs a fraction of a cent, which Apify's free plan credit covers.

Why is a site unreachable? Read its failureReason (timeout, dns_error, blocked_403 and so on). The site timed out, refused (403) or rate-limited (429) the request. The Actor already retries a blocked site once through Apify Proxy (retryBlockedWithProxy, on by default); a site that refuses that too stays unreachable. Retrying later or setting proxyConfiguration to a residential group can help.

I only need one company profile per domain. Is there something faster? Use Company Enrichment by Domain: it reads at most 3 pages per domain and returns the company name, logo, description, founding date, one official profile per social platform, the main email and phone, for $0.003 per enriched company.

Responsible use

This Actor reads publicly available web pages only. You are responsible for complying with the law that applies to you, including data protection rules (GDPR, CCPA and similar) and anti-spam law, when you store or contact the people and companies in the results.

Feedback

If this Actor saved you time, a rating on its Store page helps others find it. Problems or requests: open an issue on the Issues tab.