Website Email Finder: Pay Only for Sites With an Email · Apify
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 and email extractor for B2B lead generation: emails, phone numbers, WhatsApp numbers, contact forms and social media links (LinkedIn URLs, Facebook, Instagram) from company websites, domains or Google Maps. B2B leads and contact enrichment; pay per site with an email.

Pricing

$2.00 / 1,000 website with an emails

Rating

0.0

(0)

Developer

Data Gleaner

Data Gleaner

Maintained by Community

Actor stats

1

Bookmarked

116

Total users

84

Monthly active users

2 days ago

Last modified

Features

Limited permissions

Share

Company Contact Details & Website Email Finder: the best email per company site, MX-checked

Type an industry and a city for a lead list, or paste company websites: one row per business with the best email to write to. It decodes addresses hidden from plain-text scraping (Cloudflare email protection, split tags, encoded scripts), checks that each email's domain can receive mail (MX lookup, free), and you pay $0.002 only for sites where an email is found; every other site is free.

Find leads by industry and city

Type an industry and a city, get a lead list: e.g. dentists in Austin TX → businesses with emails, phones and socials, one row per business.

{"searchQueries": ["dentists in Austin, TX", "plumbers in Denver, CO"], "maxPlacesPerQuery": 20}

The run finds the businesses on Google Maps with our Google Maps Leads Actor (a sub-run on your account), then scans each business's own website for its emails, phones and social profiles. Each row carries the business's name, address, category, Maps phone, rating and Maps link next to the contacts. On the Apify platform, dentists in Austin, TX with 5 businesses took 34 s and returned 5 rows, 3 with an email.

Try it now: the input form is already filled with dentists in Austin, TX and 10 businesses. Click Start; on 11-01 that run took 53 s and cost about US$0.03 (10 places, 6 sites with an email). One of its rows, shortened:

{
"placeName": "Austin Dental Works",
"placeCategory": "Dentist",
"placeAddress": "4601 N Lamar Blvd #503, Austin, TX 78751",
"placePhone": "(512) 877-9822",
"placeRating": 4.9,
"website": "https://www.austindentalworks.com/",
"bestEmail": "info@austindentalworks.com",
"bestEmailDeliverable": true,
"phoneList": ["+15128779822", "+15124540300"],
"socials": {
"facebook": [{"url": "https://www.facebook.com/AustinDentalWorks"}],
"instagram": [{"url": "https://www.instagram.com/austindentalworks"}]
},
"searchQuery": "dentists in Austin, TX",
"status": "ok"
}

Export the dataset as CSV or Excel for your CRM or email tool, or read it through the API.

What each mode costs (pay per result, nothing else from this Actor):

ModeInputCost
Lead list by industry and citysearchQueriesGoogle Maps Leads' own per-place price for each business found (US$0.0015 today), plus US$0.002 per business whose website has an email. 100 businesses with 60 emails: about US$0.27
Your own websiteswebsites, startUrls or datasetIdUS$0.002 per site with an email; sites with no email are free. 1,000 sites with 600 emails: US$1.20
Lead list from OpenStreetMap (any country)osmQueriesNo charge for the listings; US$0.002 per business whose website has an email
Company namescompanyNamesCompany Domain Finder's own per-website-found price, plus US$0.002 per site with an email
Follower counts (add-on)enrichSocialProfilesUS$0.003 per social profile with a follower count read

More detail: Have a Google Maps search, not a list of websites?

Why this one

  • Free deliverability check on every email. Each address's domain is looked up in DNS (MX record) and flagged deliverable true, false or null; bestEmail prefers deliverable addresses, and a site whose only emails are on domains that cannot receive mail is returned but not billed. It is a domain check, not a mailbox test (see Measured results).
  • Finds hidden and obfuscated emails. 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.
  • Price change from 2026-11-24 (Apify's 14-day notice): the price per site with an email becomes US$0.005 on Apify's Free plan, $0.004 on Starter (Bronze), $0.0034 on Scale (Silver) and $0.0028 on Business (Gold). Sites with no email stay free. Until then every plan pays $0.002.
  • Bulk or recurring lists? Email datagleaner@gmail.com for a volume price.
  • 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 exists as an opt-in switch (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.
  • Blocks are respected. A site that answers HTTP 403, 429 or 503, or a bot-check page, is not worked around: no proxy switch, no other headers, no browser. It is reported as unreachable or blocked_challenge and is free.
  • Respects robots.txt, always. Every page is checked against its site's robots.txt before it is fetched (a robots.txt that answers with a server error skips the site), and each row's robotsBlocked says how many of that site's pages were skipped for it.

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",
"bestEmailDeliverable": true,
"emailList": ["contactus-hk@appier.com"],
"phoneList": ["+886287802800"],
"emails": [{"value": "contactus-hk@appier.com", "kind": "general", "deliverable": true, "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) 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. One exception: a site with no address on its own domain that shows or links one other domain's inbox on its home or contact page (a parent group's sales@thedhgroup.com.au on dhpainting.com.au, an initials domain such as info@bppainters.com.au on brisbaneprofessionalpainters.com.au, a school-board or ISP mailbox) gets that address in emails, marked "ownDomain": false; if two or more other domains qualify, none is kept. That address must be in the page's own HTML (not only in the optional browser render), and it is never a placeholder or vendor address: a website builder's filler text (such as filler@godaddy.com), template demo data, a chat, booking or review widget's inbox, or the web agency a "Website by" / "Powered by" / "Designed by" credit names (also in German, French, Dutch and Spanish). Those stay in otherEmails and are not billed. Addresses must end in a real top-level domain, so an Instagram handle such as @brand.official is not read as an email. 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. Personal-looking addresses (free webmail such as Gmail or Outlook, and first.last@ style named-person addresses) are left out of every field and never billed, in every country, because privacy law treats them as an individual's personal data; role and company addresses such as info@ or sales@ are kept, 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.
  • EU role mailboxes only (optional): set euRoleOnly to true and rows for EU, EEA, UK and Swiss country domains keep only generic company mailboxes such as info@ or post@. Default is off. Danish (.dk) sites, rows with a .dk address and rows with a +45 phone never return emails, whatever the setting; their phones and websites are kept. Isle of Man (.im), Hong Kong (.hk) and Macao (.mo) rows, including rows with a +44 1624, +852 or +853 phone, always keep only generic company mailboxes, because local law treats a named work address as personal marketing contact.
  • 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. Social links are returned as found on the site; the Actor never opens a social platform's pages.
  • 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": 20,
"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 30 when the run has 2 GB or more, else 10), maxConcurrencyPerDomain (1 to 5, default 2), requestTimeoutSecs (3 to 120, default 20) and proxyConfiguration (default: no proxy) tune speed and politeness. renderFallback (default false) is described in the cost section; residentialRetry and retryBlockedWithProxy are accepted but ignored since 2026-11-01, because blocked sites are never worked around. 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 per-website-found price (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.

Removed directory sources

  • Yellow Pages (US and Canada) were removed on 2 November 2026. Their terms of use do not allow scraping or automated collection of their listings. A saved input that still has yellowPagesQueries or yellowPagesCanadaQueries runs normally: those searches are skipped, nothing is fetched from either site or charged for them, and the status message says so. Use searchQueries (Google Maps) or osmQueries (OpenStreetMap) instead.
  • Gelbe Seiten (Germany) was removed on 1 November 2026. Its terms of use do not allow automated collection of its listings for commercial use. A saved input that still has gelbeSeitenQueries runs normally: those searches are skipped, nothing is fetched from Gelbe Seiten, and the status message says so. For German businesses, use osmQueries (OpenStreetMap) or your own list of websites.

Worldwide lead lists from OpenStreetMap

For any country (the US, Canada, the UK, Australia, New Zealand, France, Spain, the Netherlands and the rest), osmQueries finds businesses on OpenStreetMap, then scans each one's own website for contacts.

{"osmQueries": ["dentists in Leeds, UK", "accountants in Melbourne, Australia", "Zahnarzt in Wien"], "maxOsmResults": 100}
  • Query: a business type and a place, as dentists in Leeds, UK or plumbers | Auckland. Common business types (dentists, doctors, lawyers, accountants, estate agents, plumbers, electricians, roofers, builders, hairdressers, restaurants, hotels, gyms, car repair and about 70 more) are understood in English, German, French, Spanish, Italian, Dutch and Portuguese. Any OpenStreetMap tag works as the type too: shop=bakery in Lyon, or several at once: craft=roofer,craft=carpenter in Dublin.
  • Place: a city, town or district anywhere; adding the country (Perth, Australia, Austin, TX) avoids a namesake. The place is looked up on OpenStreetMap's Overpass service and the search covers its official boundary, or a circle around the town centre when it has none.
  • Order and size: businesses with a website come first, since those are the ones that turn into contacts. maxOsmResults takes up to 1,000 per search.
  • What it bills: nothing for the listings. Each business's website is scanned and billed the usual $0.002 per site with an email; the rest are free.
  • Output: the same place fields as the other lead lists, with leadSource: "openstreetmap", placeUrl linking the OpenStreetMap entry, placeAttribution (the required "OpenStreetMap contributors" credit and the ODbL licence name, on every OpenStreetMap row, whichever OpenStreetMap server answered), and placeEmail when the map lists an email. A business with no website gets one free row with failureReason: "no_website_on_openstreetmap".
  • Coverage: OpenStreetMap is mapped by volunteers. In big European, UK and Australian cities many businesses carry a website; in smaller towns there may be few. Data from OpenStreetMap is © OpenStreetMap contributors, under the ODbL.
  • Requests go one at a time to the public Overpass servers, with a pause between them and a fallback to another public server if one is busy, as their usage policies ask.

Run it after a Google Maps scrape or any other 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 a Google Maps places scrape 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.

Large lists: 10,000+ websites, Google Sheets, re-runs and resume

Built for big batches. Everything below is optional, and a run without these fields works exactly as before.

From a Google Sheet or CSV file. Set csvUrl to the link of a Google Sheet shared as "Anyone with the link can view" (the normal /edit link works, and the tab in gid= is read), or to any public CSV, TSV or plain-text file with one website per line. csvColumn picks the column by its header (Website), its letter (C) or its number (3); left empty, the first column named website, url, domain, site, homepage or similar is used, else the column that looks most like websites. A name, title or company column comes back on each row as sourceTitle. startUrls entries in the {"requestsFromUrl": "https://.../list.txt"} format are read the same way.

{
"csvUrl": "https://docs.google.com/spreadsheets/d/YOUR_SHEET_ID/edit#gid=0",
"csvColumn": "Website",
"maxConcurrency": 30
}

From another run's dataset. datasetId as above; set maxDatasetItems above the dataset's size (up to 100,000), since the default reads the first 1,000 items. The status message says when items were left unread.

Re-runs that skip what you already have (skipSeenDomains). With skipSeenDomains: true, every domain delivered with an email (a billed row) is remembered in a key-value store on your own Apify account, contact-details-seen-domains by default (seenDomainsStoreName sets another, one per campaign or client). Later runs with the option on skip those domains: no row and no charge. Sites that had no email are not remembered, so they are tried again next time. The skipped websites are listed in the run's key-value store under SKIPPED_SEEN_DOMAINS. To start over, delete the store under Storage in the Console.

{
"datasetId": "DATASET_ID",
"maxDatasetItems": 20000,
"skipSeenDomains": true,
"seenDomainsStoreName": "q4-dentists"
}

Resume after a migration, abort or timeout (always on). If the platform migrates the run to another server, or you abort it, or it reaches its timeout, the sites already finished stay in the dataset. Resurrect the run (Console: the run's Actions menu > Resurrect, or POST /v2/actor-runs/RUN_ID/resurrect, with a longer timeout if it ran out of time) and it continues with the sites that were not done: no site is crawled or charged twice, and lookups that ran on your account (company names, Google Maps searches) are not repeated. Rows for sites cut off by the timeout have failureReason: "run_timeout", are free, and get a full row when the run resumes, so filter those out when you export a resumed run.

Speed and memory. Reading pages is CPU work, and Apify gives a run CPU in proportion to its memory, so memory sets the speed. Since November 2026 the page parser uses about 57% less CPU, and the Actor picks its memory from your input unless you set one in Run options: 1,024 MB under 100 sites, 2,048 MB from 100 sites (and whenever renderFallback is on), 4,096 MB from 2,000 sites with maxConcurrency 30 or more. Sites are counted from websites, startUrls and companyNames, the search inputs times their per-query maximum, and maxDatasetItems for a dataset (1,000 for a CSV). Measured on Apify on 500 small-business sites from a dataset with maxConcurrency 20: 1.6 sites a second at 2,048 MB (5 minutes for all 500), 0.9 a second at 1,024 MB, 2.3 a second at 4,096 MB; the old parser did 0.5 a second at 1,024 MB. With every parser on the fast path (0.1.78) and 20 sites in parallel at 2 GB, the same 500 sites took 4.5 minutes (1.8 a second, about 30% less platform cost than 10 in parallel). The default is now 30 sites in parallel: about 4.6 minutes for the 500 at $0.067 per 1,000 sites on the platform, a further 10 to 15% cheaper than 20, and 40 gave no more. 4,096 MB with 30 finished the 500 in 2.8 minutes at a similar cost. For 5,000 sites or more, set maxConcurrency 30 to 40 (the Actor then takes 4,096 MB) and a timeout of 1 to 2 hours. The price per site with an email is the same whatever memory is used. The run keeps a fixed pool of maxConcurrency workers, so memory stays flat however long the list is. Each request has a hard time limit and each page is read up to 3 MB, so a server that drips bytes or sends a huge file cannot stall the run, and a site that runs into the run's timeout is returned with what its pages gave so far (billed only if that includes an email). The status message shows progress every 30 seconds. With a maxTotalChargeUsd set on the run, no new site starts once the budget is spent, and the run ends SUCCEEDED with the rows it has.

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"}],
"contactPageUrl": "https://www.appier.com/en/contact",
"contactFormKind": "hubspot",
"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,
"robotsBlocked": 0,
"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.

robotsBlocked is how many of the site's pages were not fetched because its robots.txt disallows them (or answered with a server error, which skips the whole site); 0 means robots.txt stopped nothing. The check is always on.

Contact page and form kind (free)

Many small-business sites publish a contact form and no email address. Every row now says where that form is, at no extra cost:

  • contactPageUrl: the site's contact page among the pages the crawl read. That is a contact page with a form first, then any other page with a form, then the home page if the form is there, then a contact page without a form. It is null when none was found.
  • contactFormKind: the kind of form on that page. A hosted form tool is named: hubspot, typeform, google-forms, jotform, microsoft-forms, tally, formspree, wufoo, formstack, cognito-forms, zoho-forms, marketo or pardot. A form the site serves itself (a plain HTML form, a WordPress form plugin, a site builder's form) is html. The value is none when the pages read show no contact form, and null when the site was not read at all (invalid, unreachable, blocked by robots.txt or by a bot check).

How it works: the fields come only from pages the crawl already fetches for emails, so a site gets no extra request. robots.txt and the bot-check rule apply as for every page. The Actor only reads pages and never fills in or submits a form. Newsletter signups, search boxes and login forms are not counted. Billing is unchanged: a row is billed only when it has an email, so a site with only a form stays free.

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 (a 429 is retried after a pause on the same route, never through another proxy).
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 (robots.txt is always checked; see robotsBlocked).
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. A free SUMMARY record in the run's key-value store (never a dataset row, so your exports and integrations are unchanged) holds the same counts: sitesCrawled, sitesBilled, sitesWithoutEmail, sitesUnreachable, sitesFreeByPrivacyScrub, osmSearches, osmUnavailable.

Why a site came back without an email, when addresses were removed. The privacy and territory rules never return free-mail or first.last@ addresses, addresses on our removal list, or addresses the destination country's rules do not allow. A row where this happened now has emailsScrubbed (how many addresses were removed) and scrubReasons (codes: freemail, personal_name, removal_list, territory_dk, territory_role_only). The addresses themselves are never output. Both fields are free and change nothing in billing. They only explain a missing email; they do not tell you whether a person's address exists or help you find it.

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, or the one other-domain address the site itself shows on its home or contact page when it has none of its own (marked ownDomain: false; never a placeholder, website-builder, widget, template or web-agency address; other 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.

Lead lists by industry and city (searchQueries): Google Maps Leads bills its own per-place price for each business it finds (US$0.0015 today) as a separate run on your account, then this Actor bills the same US$0.002 per site with an email. 100 businesses of which 60 have an email: 100 x $0.0015 + 60 x $0.002 = about $0.27.

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.

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. Measured on 2026-11-02 on 38 small-business sites (plumbers, dentists, electricians, salons in the US, UK, Canada, Australia and Germany) where the plain crawl found no email: with the fallback on, the browser rendered 35 of them (the other 3 disallow crawling) and found an email on 1. Most of these sites offer only a contact form and never show an address, rendered or not. Expect the fallback to help mainly on sites whose address is drawn by a script or a chat or booking widget, and to add about US$0.0008 of platform usage per email-less site. 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.

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.

Small-business sites (2 November 2026). On 124 small-business websites (Google Maps listings with a website, 5 trades in mid-size cities: 40 US, 21 each UK, Australia, Canada, Germany), 86 (69%) returned an email, all with a deliverable domain: US 29 of 40 (73%), Australia 19 of 21, Germany 19 of 21, UK 11 of 21, Canada 8 of 21. Of the 38 sites without an email, 22 publish only a contact or booking form. Sites with no email are not billed.

Contact-form URL and kind on sites with no email (2 November 2026). 23 of 38 no-email small-business sites came back with their contact-form URL and kind (run YP6aTuSTLzRFuL47O, 11-02): 22 html forms and 1 google-forms, free of charge on build 0.7.19.

A different 25 sites will give a different rate. Sites that block non-browser clients were the main loss in this test. Such sites are reported as blocked and free; the Actor does not work around blocks and does not solve captchas.

Retry results (October 2026, measured before the proxy retry was removed on 2026-11-01). 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) are not worked around and are 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 in Clay

Clay is a third-party product; this Actor is not made or endorsed by Clay. Clay's native Run Apify Actor action calls an Actor with your own Apify account, once per table row. For that shape of call, set perRow to true and pass the row's website as the single start URL:

{"perRow": true, "website": "acme.example"}

In the action, choose this Actor (datagleaner/website-contact-details-scraper) and put the row's Website column in the website value. website takes a bare domain (acme.example) or a full address; a bare domain gets https:// added. startUrls still works but must hold a full address starting with https:// (a bare domain there is rejected with a 400 before the run starts); if you give both, website wins and startUrls is ignored. A step-by-step guide is at https://keencrest-guides.web.app/clay-run-apify-actor-find-contact-email-per-row.html. Measured on 10 sites on 2026-11-02: median 5.5 seconds per row, 3.6 to 20.1 seconds, about US$0.0004 of platform usage per row. Map the output columns bestEmail, emails, phones and contactPageUrl; they are flat strings and lists, with no JSON path to dig through.

What perRow changes: it reads the home page and the contact page only, on a 25-second limit per site, with no browser fallback and no people extraction, and it returns one flat row. What it does not change: robots.txt, bot challenges, site terms, EU/UK role-only addresses and the .dk rule apply exactly as before, and billing is the same, US$0.002 only for a row with a bestEmail (no email: free). With more than one website in a run, perRow is ignored and the normal crawl runs. A perRow row with failureReason no_email_found or no_contact_found also carries a free retryHint field with the value retry_full_crawl; it is absent on every other row and adds no charge. To chain a second, full-crawl column in Clay: add a second Run Apify Actor column on this Actor, run it only when the first column's retryHint equals retry_full_crawl, and send {"website": "<the row's Website>"} with no perRow. That column crawls the site in full (8 pages by default, browser fallback included); the same billing rule applies, US$0.002 only if it finds a bestEmail.

For n8n, Zapier or any tool that cannot branch on a column, set fullCrawlFallback to true together with perRow: when the fast path ends with no email or no contact page, the Actor runs the full crawl on that same site in the same run and still returns one row. Trade-off: more emails found, but those rows are slower (the full crawl can take a minute or more instead of a few seconds). Same checks, same billing (US$0.002 only for a row with a bestEmail), off by default.

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: the three things that go wrong most

In an audit of 60 small-business websites from Google Maps (restaurants, dentists, agencies in New York, Denver and the UK; Apify run, October 2026), 35 returned an email. Of the 25 without one, 24 fell into the three cases below (the last site refused every connection), and none of the 25 was billed.

1. "The site has no email" (no_email_found or no_contact_found): 12 of the 25. The site publishes a contact form, a phone number or social profiles and no address at all; we checked each one by hand. Those rows still carry phoneList, contactForms and socials, so filter on phoneList for a call list. A lead list by industry and city (searchQueries) also keeps the Maps phone (placePhone) on every row.

2. "The site blocked us" (blocked_403, rate_limited_429, blocked_challenge, robots_disallowed): 9 of the 25 (15% of all sites). These sites refuse plain HTTP from datacenter IPs, serve a bot check, or disallow crawlers in robots.txt; the 2 of them we could open by hand had no email either. The Actor does not work around a block (no proxy switch, no other headers, no browser), and the row is free. Retrying later sometimes helps; failureReason tells you which case it was, and the run's final status message counts them.

3. "The email is on the page but I did not get it": 3 of the 25. The causes we found: an address drawn by a script after the page loads (switch on renderFallback, which reads 2 pages per email-less site in a browser; it helps only when a script draws the address, and on 38 email-less small-business sites it found an email on 1), an address on a different domain from the site (it goes to otherEmails, or into emails marked "ownDomain": false when it is the only inbox the site shows on its home or contact page), and a contact page under an unusual path (raise maxPagesPerSite or maxDepth). If you find a site where the email is plainly in the page and we missed it, open an issue with the URL.

More questions

Why do some local-business sites return no email? Many small-business sites publish only a contact or booking form and no address on their pages (22 of 124 in our 2 November sample, most often in Canada and the UK). They come back as no_email_found with contactForms, contactPageUrl and contactFormKind filled and are not billed. The Actor never guesses or generates an address the site does not show.

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. For 10,000+ sites, a Google Sheet as input, re-runs that skip domains you already have, and resuming a stopped run, see Large lists.

Are the emails verified? Every email's domain is checked in DNS for an MX record (or an A/AAAA record, per RFC 5321), at no extra cost, and each email carries a deliverable flag (bestEmailDeliverable in CSV exports). That tells you the domain accepts mail, not that the mailbox exists: the Actor never connects to a mail server. Sites whose only emails fail the check are not billed. For a mailbox-level check, pass bestEmail to an email verification tool of your choice.

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).

What happened to webSearchQueries? It was removed in version 0.1.78. The field is still accepted, so saved tasks and API calls keep working, but it is ignored and the status message says so. To find companies by industry and city, use a Google Maps or OpenStreetMap lead list instead.

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 does not work around a block (no proxy switch, no other headers, no browser): a site that refuses stays unreachable or blocked_challenge. Retrying later can help. If you want a proxy for every request, set proxyConfiguration.

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.

Contact family: lead list in, enriched contacts out

This Actor is the last step of a workflow. Start with whichever Actor produces the list you need, then pass its dataset here to get emails, phones and best-contact fields for every website.

Your starting pointRun firstThen
Local businesses in a city or nicheGoogle Maps Leads Scraper (emails and team-page decision makers included)Pass its dataset here to re-scan sites it could not read
Companies that are hiringHiring Signal LeadsPass the company websites here for the general inbox
Company names onlyCompany Domain FinderPass the domains here
A list of websites you already haveNothing, start hereAdd the team-page follow-up below for named people

The hand-off is one field. Put the first run's dataset ID in this Actor's datasetId input, or add this Actor under the first run's Integrations tab so it starts on its own when the first run finishes. Rows keep their sourceTitle, so you can join the enriched contacts back to the original list.

Two-step example. Find 200 dentists in Perth with Google Maps Leads, then run this Actor on that dataset. You pay the first Actor's price for the listings and $0.002 per site with an email here; websites with no email cost nothing. When you need the owner or manager by name, follow up with Company Leadership & Team Page Scraper on the same websites.

  • Google Maps Leads Scraper: local businesses from Google Maps with their phones, website emails and the decision makers named on their team pages, in one run.
  • Company Enrichment by Domain: domain to company profile (name, logo, official social profiles, contacts, founding date, tech hints), one row per company.
  • Company Leadership & Team Page Scraper: use it when you need named people (names, roles, LinkedIn profiles) from a company's team or leadership page, not the general inbox.
  • Hiring Signal Leads: use it when you want companies that are hiring sales reps right now, each with a decision-maker contact, instead of a list of websites you already have.
  • App Developer Email Finder: developer contacts for Google Play, App Store and Steam apps.

Removal requests

If you run a business or are an individual named in these results and do not want this Actor to return your domain or email addresses, email datagleaner@gmail.com (Keencrest, the publisher; replies come from an AI agent, Crest, and the business is owner-operated) with the domain or address. We add it to a blocklist (src/optout_dids.txt, plus the company-wide src/optout_shared.txt, both loaded at the start of every run) within 30 days, usually much sooner, answer you, and keep a dated record of the request and our answer. A site whose domain is listed is skipped before anything is fetched and is not billed; a listed email address is dropped from any row.

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.

Changelog

  • 2026-11-02: new opt-in input fullCrawlFallback (with perRow): a fast-path row with no email or contact page is retried with the full crawl in the same run. Slower, same billing, off by default.
  • 2026-11-02: new free field retryHint (retry_full_crawl) on perRow rows with no email (no_email_found, no_contact_found), so a Clay table can chain a second, full-crawl column. Billing is unchanged.
  • 2026-11-02: new input website: one site as a plain string, a bare domain works (it gets https://); wins over startUrls if both are given.
  • 2026-11-02: new input perRow for tables and workflows that call the Actor once per row with one website: home page plus contact page only, a 25-second limit, and one flat row (bestEmail, emails, phones, contactPageUrl). Every check and the billing rule are unchanged. See "Use in Clay".
  • 2026-11-02: new free fields contactPageUrl and contactFormKind say where a site's contact form is and which kind it is (HubSpot, Typeform, Google Forms, Jotform, a form the site serves itself, and more). They come from the pages already read: no extra requests, forms are never submitted, and billing is unchanged. A site with only a form stays free.
  • 2026-11-02: a site that answers with an active bot check (for example an AWS or Cloudflare "challenge"), on its robots.txt, any page, or in the browser fallback, is now skipped for the rest of the run: no further requests, the browser never runs the check, and nothing behind it is read. The row says failureReason: "blocked_challenge" and is free. Before, an empty answer of this kind to robots.txt was read as "no robots.txt", and the browser fallback could pass the check. Sites that send unusual characters in a response header no longer come back unreachable.
  • 2026-11-02: corrected the renderFallback yield in this README: on 38 email-less small-business sites it found 1 email, not the 5% stated earlier. A robots.txt that answers a server error (5xx) is now retried once after a short pause before the site is skipped; 403 and challenge pages are never retried.
  • 2026-11-02: with renderFallback on, a site whose pages the browser rendered is no longer reported as failureReason: "timeout" just because it waited for a free browser slot. timeout now means the plain crawl ran out of time, or every page the browser tried timed out.
  • 2026-11-02: fixed a bug in builds from 2026-11-01 12:04 UTC to 2026-11-02 01:46 UTC where some placeholder or vendor addresses (for example a website builder's filler address) on a site's pages were billed as the site's email. These are no longer billed. A web agency's inbox named in a site's "Website by" or "Powered by" credit is no longer billed either.

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.