Website Email Extractor for Google Maps Leads & Domain Lists
Pricing
from $1.60 / 1,000 processed websites
Website Email Extractor for Google Maps Leads & Domain Lists
Website email extractor and contact details scraper for bulk domain lists: best email per site with MX check, phones and social links. Also adds emails to Google Maps results (does not scrape Maps itself). $2/1,000 sites; nothing found = free.
Pricing
from $1.60 / 1,000 processed websites
Rating
0.0
(0)
Developer
Yukai Lin
Maintained by CommunityActor stats
0
Bookmarked
42
Total users
19
Monthly active users
5 hours ago
Last modified
Categories
Share
What does this contact scraper do?
Paste a list of websites, get one row per website with its emails, phone numbers and social profiles (LinkedIn, Facebook, Instagram, X, YouTube, TikTok and more), ready for CSV, Google Sheets or your CRM. Company names (with a city) and a Google Maps scraper's dataset work too; this Actor reads the listed websites and does not scrape Google Maps itself.
$2 per 1,000 websites, charged only when a contact is found. One price per website, however many pages are read. Websites with nothing found, unreachable websites and duplicates are free. No start fee.
| You paste | You get (one row per website) |
|---|---|
hofbraeuhaus.de | bestEmail willkommen@hofbraeuhaus.de, primaryPhone +4989290136100, charged: true |
stumptowncoffee.com | bestEmail info@stumptowncoffee.com, primaryPhone +18777113385, Instagram, Facebook, X, YouTube, TikTok and Pinterest profiles, charged: true |
Real results from our checks: hofbraeuhaus.de home page only, October 19, 2026 (the prefilled example); stumptowncoffee.com with default settings, September 2026 (full row). Live sites can change.
Try it in a minute: the form is prefilled with hofbraeuhaus.de, reading only its home page (at most $0.002). Click Start, then paste 10–20 of your own domains (at most $0.04 at the Free/Bronze rate) and check the rows and SUMMARY; see First run.
Price by plan: Free and Bronze $0.002 per website with a contact ($2/1,000); Silver $0.0018; Gold and above $0.0016. With requiredContact:"email", only sites with an email are charged; failed or unreachable sites are free.
Coming from Contact Details Scraper (vdrmota/contact-info-scraper)? Paste your existing startUrls input as is: it runs without changes, and you pay per website with a contact, not per page. See Switching from another contact scraper.
Choose your input:
- Domain list: paste websites one per line.
- Company names: add a city or address to each name.
- Google Maps dataset: run a Maps scraper first, then supply its dataset ID. This Actor does not scrape Google Maps.
How to find emails from a list of domains
- Paste your company websites into Websites or company names, one per line (
example.comorhttps://example.com/about; a column copied from a spreadsheet works). - Optional: set Pages per website (default 8) or turn on Role mailboxes only to skip named people's addresses.
- Click Start. Each website gets one row with
bestEmail, all emails (with an MX check), phones and socials. A website with nothing found, or that cannot be reached, is a free row that says why. - Export as CSV, Excel or JSON. 1,000 websites cost at most $2.
After a completed run, the Output tab offers Leads CSV (overview columns) and Contact coverage report (Markdown). The CSV uses the existing overview columns; the report is saved as CONTACT_REPORT in the run's key-value store, including runs where every input is invalid, a platform page, or skipped by onlyMissingEmail. It counts unique websites queued, successfully read this run, found with a website email, read without one, failed by errorType, and left unprocessed. If dedupeStoreName reuses results from an earlier run, the report counts those free, older rows separately and includes their website emails in delivered coverage. Invalid and skipped input rows are separate from website coverage. For example, 100 queued websites with 70 finding an email, 20 read without an email and 10 blocked or failed are reported with separate denominators, not as 100 verified leads. Failed website rows are free. PARTIAL_RESULTS keeps completed CSV rows and points to RETRY.input for retryable rows; a charge-limit stop lists unprocessed inputs in SUMMARY.notProcessed. An MX record is a domain-level signal, not proof that an individual mailbox accepts delivery. CSV emails are publicly found addresses, not delivery-verified leads. If report storage fails, the run fails instead of claiming a usable report link; saved dataset rows, charges and SUMMARY remain.
First run
- Choose an input above. For domains, paste one per line. For company names, include the city or address on each line.
- Start with 10–20 websites: it costs at most $0.04. In our October 2026 test, 20 websites finished in 26 seconds; slow websites can take longer. Before reading anything, the log shows the most the run can cost.
- Before processing a larger list, check the sample's
SUMMARYstatus and failure counts, then inspect rows witherrorTypeandcharged: false.PARTIAL_RESULTScan mean some websites could not be read (network,not_found,blocked,timeout...; 11 of 100 in our October 2026 test). Those failed rows are free; completed rows remain available. See the FAQ for failure details. - What is charged:
requiredContact: "any"is the default, at $0.002 per website where at least one email, phone number or social profile was found. If you only need emails, optionally chooserequiredContact: "email"so a website with only a phone or social profile is free. Websites with nothing found, unreachable websites, invalid lines and duplicates are free; every row showscharged: true/false. - $5 pays for 2,500 charged websites at the Free and Bronze plan price. Free rows do not count, so a longer list can cost less. To cap a bigger run, set Maximum charge per run in the run options.
- If later lists overlap, optionally use the same
dedupeStoreNameacross runs: earlier stored website results are returned withfromPreviousRun, without reading or charging for them again. Failed websites are not stored, so they can be tried again. Reused results may be older than a fresh read;dedupeMaxAgeDayscontrols their maximum age (default 90 days;0means no age limit). See Fill the missing emails in a list you already have.
How to extract emails from Google Maps leads
- Run any Google Maps scraper (for example compass/crawler-google-places) for your search, or have your list of company websites ready.
- Put that run's dataset ID in Dataset ID, or paste the domains into Websites or company names, one per line.
- Click Start. Each place or website gets one row with
bestEmail, all emails (with an MX check), phones and socials; the MapsplaceIdis kept so the rows merge back. - Export as CSV, Excel or JSON. 1,000 places cost at most $2; source rows without a usable website and websites with nothing found are free.
It does not scrape Google Maps itself: run any Google Maps scraper first (for example compass/crawler-google-places), then point this Actor at that run's dataset. It reads the website listed for each place (home page plus contact, imprint and about pages) and returns one row per place, with the place's name, address, phone, Google Maps link and placeId kept, so the emails merge straight back into your Maps export.
For a Maps source row with no usable website listed, the free output row has errorType: "no_website" and websiteStatus: "not_listed_in_source". Filter on websiteStatus to review these candidates. This only means the dataset you supplied did not list a usable website: its scraper may have missed one, and the business may have an official website in another source. No website search is made for these Maps rows.
For API or spreadsheet workflows, mapsData accepts a pasted Maps JSON array, an { "items": [...] } object, or CSV with a website column. An Apify dataset URL can also be passed as datasetId. The optional mapsPlaceRows: true returns flat placeName, address, category, website, emails, phones, socials, source, and sourceIndex columns; the default is off, preserving this Actor's existing output. The listing phone is included in the optional phones column but does not by itself trigger a charge.
Company names instead of websites
A line that is a company name (Katz's Delicatessen) is looked up to its website. Give the city, address or phone with the name, one business per line: Joe's Pizza, 7 Carmine St, New York, NY 10014, or columns pasted from a spreadsheet (tab-separated, or with a header row such as Name,City,Phone). The same works for rows or a dataset that has a name column but no website column. With a city, the name is first looked up in our places database (the open Overture Maps places: a listing with the same name in that city, ideally the same phone or street, that gives a website); then Wikidata's official-website property (P856, strict name match; with a city or phone in the row, Wikidata's headquarters or phone must match too); then the likely domains are checked (joespizzanyc.com, the country's domains first: .co.uk, .de, .com.br, .com.tw...). A website is used only when verified: its home page names itself as the business (schema.org, og:site_name or the title), or shows the row's phone; with a city or address in the row the page must also show the city, street, postcode or phone, so a same-name business elsewhere is not taken. Parked or for-sale pages, and a guessed domain that redirects to an unrelated site, never count. When the website was found through its places listing and shows no email or phone itself, the listing's email or phone is added with source: "overture-places" and is not charged. The row shows how it was found (websiteFromName: method, evidence, listing). Names without a verified website get a free row (errorType: "company_not_found") with the reason and a hint on what to add (for example "add a city or address column"); SUMMARY companyNamesMessage says how many matched. The website is then read and charged like any other ($0.002, only when a contact is found); the lookup itself costs nothing. Our October 13, 2026 check of 40 local businesses in the US, UK, Germany, Taiwan and Brazil: names only 28% found; name + city 82%, name + address 85%, name + address + phone 87%, with no wrong website when the row had a city or address. Names alone are ambiguous (there are many "Joe's Pizza"): add the city.
Why this one
- Made for Google Maps leads: reads the dataset of compass/crawler-google-places, compass/google-maps-extractor and similar scrapers as is (the
websitefield is found automatically, the Mapsurlis never read as a website). Source rows without a usable website get a free row, so every place maps back 1:1; for places with only an Instagram or Facebook page, the company's own website is looked up by name and address. - One best email per business, checked for free:
bestEmailpicks a person's address on the company's domain, then a shared inbox (info@, sales@) on the company's domain, then a free-mail address (gmail.com...). Every email has a syntax check, an MX check (does the domain receive mail?),personal/roleand free-mail flags, at no extra cost. - $2 per 1,000 websites, charged per website, not per page or per email (Gold and above $1.60), with no start fee. Websites with nothing found are free, and so are unreachable websites, Maps source rows without a usable website and duplicates (branches of a chain sharing one website are charged once). Every row says
charged: true/false; withrequiredContact: "email"you pay only for websites where an email was found. - Finds hidden emails: obfuscated addresses ("info [at] firm [dot] de"), Cloudflare-protected and script-encoded emails are decoded, and junk such as
logo@2x.pngoryou@example.comis filtered out. JavaScript-only websites are read with a browser at no extra charge; if Cloudflare browser capacity or its monthly usage limit is reached, our Chrome pool is tried. No API key, login or proxy needed. - CSV-ready: flat columns (
businessName,bestEmail,emailsText,placeId,mapsUrl...) for Sheets, CRMs and outreach tools, or one row per email for Instantly and lemlist.
Input: domain list or Google Maps results (copy and paste)
A plain list of domains (the most used input), one website per entry:
{"domains": ["stumptowncoffee.com", "hofbraeuhaus.de"]}
Or the results of a Google Maps run: put its dataset ID (Console: the run's Storage tab, or defaultDatasetId in the API) in Dataset ID. Nothing else is needed:
{"datasetId": "YOUR_GOOGLE_MAPS_DATASET_ID","requiredContact": "email"}
Output: one row per place (shortened)
placeName, placeAddress, placePhone, mapsUrl and placeId are copied from your Maps dataset (illustrative values here); contact fields come from the business's website when a usable one is listed. A free Maps row with no usable source website has errorType: "no_website", websiteStatus: "not_listed_in_source", charged: false and the same place columns. The status describes the supplied row only; it does not verify that the business has no official website.
{"businessName": "Stumptown Coffee Roasters","placeName": "Stumptown Coffee Roasters","placeAddress": "128 SW 3rd Ave, Portland, OR 97204","placePhone": "+15032241000","placeId": "ChIJ...","mapsUrl": "https://www.google.com/maps/search/?api=1&query=...&query_place_id=ChIJ...","domain": "stumptowncoffee.com","bestEmail": "info@stumptowncoffee.com","bestEmailType": "role_company","bestEmailHasMx": true,"emailsText": "info@stumptowncoffee.com, legal@stumptown.com, customerservice@stumptown.com","primaryPhone": "+18777113385","socialProfiles": { "instagram": "https://www.instagram.com/stumptowncoffee", "facebook": "https://www.facebook.com/stumptowncoffee" },"charged": true}
The website values above are the real result for stumptowncoffee.com (September 2026; MX checked October 4, 2026). It was charged once, $0.002, whatever the number of pages read. All fields, where each value was found and the pages checked: Output example.
How much does it cost?
| Event | Price |
|---|---|
| Processed website | $2.00 / 1,000 websites ($0.002 each) |
Example: 2,000 websites, of which 1,500 have at least one contact, cost 1,500 × $0.002 = $3.00; the other 500 are free.
No start fee. One price per website, whether 1 or 20 pages are read. You pay only when at least one email, phone number or social profile is found. Websites with nothing found, unreachable websites (they carry an errorType: blocked, network, not_found, timeout…), invalid lines and duplicates are free. Every row has charged: true or false. Higher Apify plans get volume discounts: $1.80 per 1,000 websites on Silver, $1.60 on Gold and above.
Features
A bulk email extractor for a list of domains or for Google Maps results: give it company or business websites (URLs or bare domains, up to 50,000 per run) or the dataset of a Google Maps run, and get one clean record per website:
- 📧 Emails:
mailto:links, addresses written in the text, obfuscated addresses ("info [at] firm [dot] de", "info(at)firm.de", the full-width "info@firm.com.tw", addresses written backwards or put together by a script, ROT13decryptMail(...)links, WordPress email-encoder plugins) and Cloudflare-protected emails (decoded). Lower-cased, de-duplicated, and junk is filtered out: image file names such aslogo@2x.png, placeholder and example addresses (you@example.com,name@domain.com), error-tracking IDs and no-reply addresses. - 📞 Phone and fax numbers in international format (
+12122542246), fromtel:links, WhatsApp chat links (wa.me/…), schema.org data and the page text. Every number is checked with Google's libphonenumber rules for its country, and numbers in text are only taken after a label ("Tel:", "Phone", "電話"...; the next numbers of the same list too), in international format, or in a typical phone layout, so order numbers, dates, prices and example numbers are not picked up. - 👥 Social profiles: Facebook, X, LinkedIn, Instagram, YouTube, TikTok, GitHub, Pinterest, Threads, Trustpilot, Discord, Telegram and WhatsApp. Only real profile URLs count (share buttons, posts and videos are ignored), and default profiles from website templates (e.g.
facebook.com/wix) are skipped. - 🏢 Organization name, logo and postal address from the site's schema.org data (JSON-LD or Microdata: Organization, LocalBusiness, Restaurant, Dentist...).
- 📝 Contact form URL: the page with a contact form (HTML forms with a message box, or HubSpot, Contact Form 7, Typeform, Jotform and other embeds).
- ✅ Free email quality signals: for every email a syntax check (
syntaxValid), an MX check (hasMx: the domain publishes mail servers),emailType(rolefor shared inboxes such as info@ or sales@,personalfor the rest) andfreeMail(gmail.com, yahoo.com, web.de...), plus onebestEmailper website (see Best email). No mail server is contacted, so this is not a mailbox verification. - 📍 Google Maps columns kept: with a Google Maps dataset each row gets
placeName,placeAddress,placePhone,mapsUrl,placeId,placeCategory,placeCityandplaceCountryCodefrom its place, andbusinessName(the Maps name, else the website's organization name). - 🔎 Where each value was found: the page and the method (
mailto,text,obfuscated,cloudflare,jsonld,attribute,script,tel-link,whatsapp), plus the list of pages checked.
For each website it reads the home page plus likely contact pages: contact, imprint/Impressum (including German "Rechtliche Angaben", "Rechtliches" and "Anbieterkennzeichnung" links), locations, about, team, support and legal pages (in English, German, French, Spanish, Portuguese, Italian, Dutch, Japanese, Chinese and Korean wording), up to 8 pages by default. When the site's own email and a phone number are already found, the remaining team, support and privacy/terms pages are skipped.
Use cases
- Google Maps leads to emails: add the best email, all emails and social profiles to the places from a Google Maps scraper, merged back by
placeId - Lead generation: turn a list of company domains into emails, phone numbers and social profiles
- Enrich directory results: add emails and socials to businesses that only list a website
- CRM clean-up: find current contact details and social accounts for your accounts
- Outreach research: find the press, sales or support address of each company
Cost control and price comparison
Two billing modes (requiredContact)
| Mode | Charged ($0.002 per website) | Free |
|---|---|---|
any (default) | at least one email, phone number or social profile found | nothing found, unreachable, invalid, duplicate |
email | at least one email address found | everything else, including websites with only a phone number or social profile (still returned in the dataset with charged: false) |
Use email for cold-email lists where a phone number or social link is of no use to you: you pay only for rows you can email. The price per website is the same in both modes; email can only cost less.
Control your cost
- Before anything is read, the run logs its plan: the number of websites × the price = the most the run can cost, compared with your maximum charge per run.
- Charged: a website where at least one email, phone number or social profile was found ($0.002). With
requiredContact: "email", only a website where at least one email was found is charged (same $0.002; a website with only a phone number or social profile is returned but free). - Free: websites with nothing found, unreachable or blocked websites, company names whose website could not be verified (
company_not_found), other lines that are not a website, and duplicates:acme.com,www.acme.comandacme.com/enare one website, charged once. There is no extra charge from us for more pages, the browser fallback or our own network; only an Apify Proxy you choose yourself is billed by Apify. - Maximum charge per run: set it in the run options (Console) or with
maxTotalChargeUsd(API). When the next website could exceed it, no new website is started; the run ends with statusLIMIT_REACHED, and theSUMMARYrecord lists the websites not processed (notProcessed: the count and up to 100 inputs) so you can run them again with a higher limit. - If Apify restarts the run (server migration or Resurrect), items already finished are skipped and not charged again (
SUMMARY.resumedSkipped).
Price comparison (checked September and October 2026)
| Actor | Price | Cost for 1,000 websites at 5 pages each |
|---|---|---|
| Website Email Extractor (this Actor) | $0.002 per website with contacts found | $2 at most (websites with nothing found are free) |
| vdrmota/contact-info-scraper | $0.002 per page scraped | $10 |
| compass/crawler-google-places, contacts add-on | $0.002 per place, only inside a new Google Maps scrape (plus $0.004 per place scraped) | $2 for contacts (+ $4 for the places) |
| lukaskrivka/google-maps-with-contact-details | $0.0025 per place for contacts, only inside its own Google Maps scrape (plus $0.005 per place) | $2.50 for contacts (+ $5 for the places) |
vdrmota/contact-info-scraper is the most used Actor in this category (about 2,380 users in the last 30 days on October 2, 2026) and can follow links more deeply; it charges per page. This Actor charges per website where something was found and picks the likely contact pages for you.
How to use it
- Domain list: paste domains or website URLs into Websites or company names, one per line (a column copied from a spreadsheet works). Blank lines are ignored; a company name is looked up to its website (see Company names instead of websites); a line that is neither gets one free
invalid_inputrow. Google Maps results: put the dataset ID of your Google Maps run in Dataset ID (see Find emails for Google Maps results). Or pick another run's dataset (see below). - Optionally change Pages per website (1–20, default 8).
- Click Start and export the results as JSON, CSV or Excel.
Input example
This is the one-site input prefilled in the form. It reads one public homepage; the local extraction check took 4.7 seconds on October 19, 2026 (Apify run time can differ):
{"domains": ["https://www.hofbraeuhaus.de/"],"maxPagesPerSite": 1}
Output example (real result, September 2026)
{"input": "stumptowncoffee.com","inputIndex": 0,"url": "https://stumptowncoffee.com/","domain": "stumptowncoffee.com","finalUrl": "https://www.stumptowncoffee.com/","success": true,"organizationName": "Stumptown Coffee Roasters","primaryEmail": "info@stumptowncoffee.com","primaryPhone": "+18777113385","emailsText": "info@stumptowncoffee.com, legal@stumptown.com, customerservice@stumptown.com","phonesText": "+18777113385, +18332672844, +18557113385","emails": ["info@stumptowncoffee.com", "legal@stumptown.com", "customerservice@stumptown.com"],"phones": ["+18777113385", "+18332672844", "+18557113385"],"faxes": [],"socialProfiles": {"instagram": "https://www.instagram.com/stumptowncoffee","facebook": "https://www.facebook.com/stumptowncoffee","x": "https://twitter.com/stumptowncoffee","youtube": "https://www.youtube.com/channel/UC0yk_H5Np-uGvy98UM-jj6w","tiktok": "https://www.tiktok.com/@stumptowncoffee","pinterest": "https://www.pinterest.com/stumptowncoffee"},"address": "700 SW 5th Ave, 3rd Floor, #400, 97214 Portland, OR","logo": "https://customers.seomanager.com/knowledgegraph/logo/stumptowncoffee_myshopify_com_logo.png","contactFormUrl": null,"emailDetails": [{ "email": "info@stumptowncoffee.com", "page": "https://www.stumptowncoffee.com/", "method": "jsonld", "sameDomain": true, "mailbox": "role" },{ "email": "legal@stumptown.com", "page": "https://www.stumptowncoffee.com/pages/terms-and-conditions", "method": "text", "sameDomain": false, "mailbox": "role" }],"phoneDetails": [{ "number": "+18777113385", "display": "877-711-3385", "national": "(877) 711-3385", "country": "US", "type": "phone", "page": "https://www.stumptowncoffee.com/", "method": "jsonld" }],"pagesChecked": [{ "url": "https://www.stumptowncoffee.com/", "kind": "home", "via": "direct", "httpStatus": 200 },{ "url": "https://www.stumptowncoffee.com/pages/contact-us", "kind": "contact", "via": "direct", "httpStatus": 200 },{ "url": "https://www.stumptowncoffee.com/pages/locations", "kind": "locations", "via": "direct", "httpStatus": 200 },{ "url": "https://www.stumptowncoffee.com/pages/our-story", "kind": "about", "via": "direct", "httpStatus": 200 },{ "url": "https://www.stumptowncoffee.com/pages/terms-and-conditions", "kind": "legal", "via": "direct", "httpStatus": 200 }],"charged": true,"via": "direct"}
A German restaurant (hofbraeuhaus.de) returned willkommen@hofbraeuhaus.de (mailto link), +4989290136100 (written as "+49 (0)89 – 290 136 100" in the text) and its Facebook and Instagram profiles. A Munich food retailer (dallmayr.com) returned info@dallmayr.de, decoded from Cloudflare's email protection.
For spreadsheets and CRMs, primaryEmail is an email on the website's own domain (else the first email), a role mailbox such as info@, contact@, hello@ or sales@ before a person's address, primaryPhone the first phone number (not a fax), and emailsText / phonesText hold all of them separated by commas. input is the line you gave; when several lines were the same website they are merged into one row and listed in inputs (a page path such as hofbraeuhaus.de/en is read as an extra page, kind: "given"). Emails on the website's own domain come first (sameDomain: true); others (a parent company, an agency, a data-protection officer) follow. mailbox in emailDetails is role for known shared inboxes (info@, sales@, support@, press@…) and other for the rest (often a person's address, but not always), so you can filter shared inboxes out or in. Profiles linked only once, e.g. from a blog post, are not reported as the company's own. via says where each page was read from: direct (Apify's network), backend (our servers) or browser. A SUMMARY record in the key-value store has the run status (SUCCESS, PARTIAL_RESULTS, FAILED, NO_RESULTS, LIMIT_REACHED) and counts of websites with emails, phones and social profiles, charged websites, invalid lines and merged duplicates.
Best email and email quality
Every website row has bestEmail, the one address to use for outreach, picked in this order:
bestEmailType | Meaning |
|---|---|
personal_company | not a known shared inbox, on the website's own domain (often a person: maria@acme.com) |
role_company | a shared inbox on the website's own domain (info@, contact@, sales@...) |
free_mail | a free-mail address (gmail.com, yahoo.com, outlook.com, web.de...), common for small businesses |
other_domain | another domain (a parent company or an agency) |
Addresses whose domain has no mail server (no MX record, a "null MX" or a domain that does not exist) or with broken syntax are never picked. bestEmailHasMx is true when the domain publishes mail servers, false when it does not, and null when the DNS lookup failed. primaryEmail is unchanged (a shared inbox on the website's own domain first), so existing integrations keep working.
Each entry of emailDetails also has syntaxValid, hasMx, emailType (personal or role), freeMail and duplicateEmail (true when the same address was already returned for another website earlier in the run, e.g. one agency address on many client sites). personalEmailsText and roleEmailsText list them by type. These checks are free: no extra event and no extra charge. They use DNS only and never contact a mail server, so a mailbox that does not exist can still bounce; for a verdict and a ranked send list per address, run the results through Email List Cleaner (see Lead-cleaning workflow).
Match a supplied person's name to a published email pattern
For a source dataset or rows with a website and a person's full name, set targetPersonNameField to that name column. Leave it empty to look for personName, contactName or fullName in each source row; a company Name column is never used as a person's name. For example: {"rows":[{"website":"example.org","contactName":"Jane Doe"}]}. The Actor adds targetPersonName, namePatternMatchedEmail, namePatternMatch and namePatternMatchBasis to rows that have a supplied person name. namePatternMatchedEmail is either an address already present in that website's emailDetails, or null. The basis says that the email's local part (before @) matches the supplied full name as text; it does not verify identity or mailbox ownership. The original page remains in the matching emailDetails[].page (or emailFoundOn in email row mode). If the site publishes only info@, the match is null and namePatternMatch is false; no jane@ address is generated. Non-Latin names, initials, multiple matching published addresses, duplicate source people with the same name on one website and no matching address also return null/false. If no person name is supplied, these four columns are absent, as before. The website is still charged at most once under the existing contact rule.
One row per email (for Instantly, lemlist and CRM imports)
Set Output format to One row per email ("outputMode": "emailRows") to get one dataset row per email instead of one row per website, the shape outreach tools import. Each row has email, isBestEmail, emailType (personal or role), emailHasMx, emailSyntaxValid, emailFreeMail, duplicateEmail, isPrimaryEmail, emailSameDomain, emailMailbox (role or other), emailFoundOn (the page) and emailMethod, plus the website's domain, businessName, organizationName, bestEmail, primaryPhone, socialProfiles, socialProfileSources, address, source and, for Google Maps input, the place columns (placeId, placeName...). When a person name is supplied, every email row also keeps targetPersonName, namePatternMatchedEmail, namePatternMatch and namePatternMatchBasis. socialProfileSources maps each found social platform to the website page where its profile link was found; when no source page was recorded, no page is added. The mapping is also kept on the single email: null row for a website with a phone or social profile but no email. Use the One row per email table view, or export the dataset as CSV.
{ "domains": ["katzsdelicatessen.com", "hofbraeuhaus.de"], "outputMode": "emailRows" }
The price does not change: each website is still charged once, whatever the number of emails, and only its first row shows charged: true. A website with a phone or social profile but no email gives one row with email: null. The default, One row per website ("sites"), is unchanged.
Find emails for Google Maps results
Google Maps lists a website and a phone number for most businesses, but no email. This Actor reads each listed website and adds the emails, the best email and social profiles to every place: at most $2 per 1,000 places, and places with nothing found are free. It works with the datasets of compass/crawler-google-places, compass/google-maps-extractor, lukaskrivka/google-maps-with-contact-details and other scrapers with the same fields (title, website, phone, address, url, placeId), and with Maps exports that use name, site/website, full_address and place_id.
What you need to set: only the dataset ID. The website field is found automatically (website; the Maps url field is the Google Maps page and is never read as a website), and each output row gets these flat columns from its place:
| Column | From the Maps item |
|---|---|
placeName | title (or name) |
placeAddress | address (or full_address) |
placePhone | phoneUnformatted, else phone |
mapsUrl | url (the Google Maps link) |
placeId | placeId (or place_id); also the row's rowId |
placeCategory, placeCity, placeCountryCode | categoryName, city, countryCode |
Website values are cleaned first: Google redirect links (google.com/url?q=...) are unwrapped, tracking parameters such as ?utm_source=gmb do not matter (websites are merged by domain), and a "website" that is itself a Google Maps link counts as no website.
Recipe 1: automatically after every Google Maps run (Apify Console integration).
- Open your Google Maps scraper task (or Actor) and go to the Integrations tab.
- Choose Connect Actor or task and pick this Actor (tidytools/website-contact-extractor).
- Use this input;
{{resource.defaultDatasetId}}is replaced with the dataset of the Google Maps run that just finished:
{ "datasetId": "{{resource.defaultDatasetId}}", "requiredContact": "email", "dedupeStoreName": "my-maps-leads" }
requiredContact: "email" charges only places where an email was found; dedupeStoreName skips websites already read in earlier runs (not charged again), useful when your searches overlap.
Recipe 2: from your own code or a webhook. When the Google Maps run succeeds (an Apify webhook on ACTOR.RUN.SUCCEEDED, or your script polling the run), start this Actor with the run's dataset:
curl -X POST "https://api.apify.com/v2/acts/tidytools~website-contact-extractor/runs?token=YOUR_TOKEN" \-H "Content-Type: application/json" \-d '{"datasetId":"MAPS_RUN_DEFAULT_DATASET_ID","requiredContact":"email"}'
Then export this run's dataset with the googleMaps view (/items?view=googleMaps&format=csv) and join it to your Maps export on placeId.
Once, for a run you already have: set Dataset ID to the dataset of that run and start. To copy more fields of the place into the output (e.g. totalScore, reviewsCount), list them in Dataset fields to keep: they go into a source object. Any dataset with a website field works the same way, e.g. a directory or trade-show exhibitor list (set Dataset field when the field has an unusual name).
Every item of the dataset gets exactly one output row, so a Google Maps export joins back 1:1 (by placeId, rowId or datasetItemIndex):
- A place whose source row lists no usable website gets a row with
errorType: "no_website",websiteStatus: "not_listed_in_source"and its place columns (not charged). This does not establish whether the business has an official website elsewhere. - A LinkedIn, Facebook, Instagram, X, TikTok, YouTube, Yelp, Tripadvisor, Google Maps, OpenTable, Booking.com, Linktree or similar page, or a business directory (411.ca, Yellow Pages, PagesJaunes, Gelbe Seiten...), given as the website is never crawled as the company's website (that would return the platform's own details) and never logged into. Instead: if the same row has another website column (e.g.
Domain), that website is read (websiteFromRow); otherwise the website is looked up from the row's business name, or the name in the page's address when it clearly is one (facebook.com/JoesPizzaNYC-> "Joes Pizza NYC"), together with the row's city or address (websiteFromName.platformPage); otherwise the row haserrorType: "platform_url"with ahint("this is a LinkedIn page, not the company's website — add the website, or the company name and city") and the profile insocialProfiles. These rows are free; only a real company website found this way is charged as usual, when it has contacts.SUMMARY.platformPagescounts them. - Branches of a chain that share one website are read and charged once; each other branch gets a copy of the result with its own place columns,
charged: falseandduplicateOf. - When a website redirects to a different domain (for example an expired domain now parked on a directory), the row has
redirectedToand awarning: the details found may belong to that other site. A business directory's own mailboxes (for example help@411.ca) are never returned as the business's email, and neither are addresses of big platforms named in a privacy policy (google.com, facebook.com, paypal.com...): those go todroppedEmails.
Real example (September 30, 2026). 30 Google Maps places from compass/crawler-google-places (10 coffee shops in Portland, 10 dentists in Austin, 10 bakeries in Munich), with datasetKeepFields: ["title", "placeId"]:
| Result | Places |
|---|---|
Output rows (one per place, each with its placeId) | 30 of 30 |
| No usable website listed in the supplied Maps source row | 1 (free) |
| Instagram or Facebook page instead of a website | 3 (free unless the company's own website is found) |
| Website no longer exists | 2 (free) |
| Websites read with contacts found (charged) | 24 |
| … with an email | 21 (70% of all places, 88% of the websites read) |
| … with a social profile | 23 |
Cost: 24 × $0.002 = $0.048 (about $1.60 per 1,000 places); run time 12 seconds. Google Maps already lists a phone number for almost every place, so the email and the social profiles are what this step adds.
Lead-cleaning workflow: from Google Maps to a verified send list
For sales teams who want a list they can import into a CRM or outreach tool: Google Maps results (or a domain list) → this Actor → Email List Cleaner (tidytools/bulk-email-validator, $0.50 per 1,000 addresses) → a ranked "Ready to send" CSV → optionally Lead & Company Enrichment for industry, size and company social profiles.
- Run this Actor on your Google Maps dataset or domain list with
"requiredContact": "email"(only websites where an email was found are charged). - In this Actor's (or your task's) Integrations tab choose Connect Actor or task, pick Email List Cleaner and use this input:
{ "datasetId": "{{resource.defaultDatasetId}}" }
- The validator checks every address in
emails(or onlybestEmailif you set its Dataset field tobestEmail), keeps each row'sbusinessName,placeId,websiteandrowId, and savesREADY_TO_SEND: a CSV sorted deliverable → risky → undeliverable, each business's best email first. Join it back to your Maps export onplaceId.
The same chain also works as an Apify webhook (ACTOR.RUN.SUCCEEDED on this Actor starting the validator with the run's defaultDatasetId) or as one n8n flow (Google Sheet of domains → this Actor → validator → a "Ready to send" sheet tab).
At the end of every run the log and SUMMARY.nextSteps give the exact input for Email List Cleaner and Lead & Company Enrichment with this run's dataset ID and the most each step can cost, ready to paste (nothing is started for you).
4. Company data for each lead (optional). Connect Lead & Company Enrichment (tidytools/company-website-enrichment, $0.006 per company) after the validator, with the same { "datasetId": "{{resource.defaultDatasetId}}" } input. Every validated address keeps its row (email, sendStatus, businessName, placeId, website) and gets what the company does, its industry, B2B/B2C, the employee band when the company states it, and the company's social profiles; several addresses of one business are read and charged once. On the test below this step added $0.108 (18 companies for 35 rows), so the whole chain cost about $0.17 for 20 websites.
Customer-owned list example (20 businesses; illustrative, not a test run). A customer who has the right to use a CSV with 20 rows (row_number,Business,Website,Email) uploads it as an Apify Dataset; Website contains each business's own site, not a Google Maps or directory page. TidyTools does not supply the source list or collect Google Maps results for you. Use the customer's Dataset ID in the contact step:
{ "datasetId": "CUSTOMER_DATASET_ID", "datasetField": "Website", "rowIdField": "row_number", "onlyMissingEmail": true, "findWebsitesForNames": false, "requiredContact": "email", "respectRobotsTxt": true }
If wanted, run Email List Cleaner on the contact result with { "datasetId": "CONTACT_RUN_DATASET_ID" }; it checks email addresses, not web pages, and has no respectRobotsTxt setting. For company data on the original 20 rows, run Lead & Company Enrichment separately with:
{ "datasetId": "CUSTOMER_DATASET_ID", "datasetField": "Website", "findWebsitesForNames": false, "respectRobotsTxt": true }
Both website-reading steps now skip pages disallowed by robots.txt. A row without a usable website stays free with errorType: "no_website" (name lookup is off); when robots.txt disallows the home page, it is not read and the free row has errorType: "robots_disallowed". A site with no email is also free in the contact step because requiredContact: "email". At current Free-plan list prices, the maximum for the two website steps is 20 × $0.002 per successful contact website + 20 × $0.006 per successfully enriched company = $0.16; Email List Cleaner adds at most $0.0005 × the number of unique addresses actually checked. For 250 distinct businesses the same ceiling is 250 × ($0.002 + $0.006) = $2.00, plus email checks. These are ceilings, not success-rate estimates; skipped, failed and duplicate websites can lower the charge. This robots-on recipe is optional: the Actors' respectRobotsTxt default remains false.
Real numbers (our test run, 4 October 2026): 20 public websites of small food businesses (bakeries, delis, restaurants). This Actor read all 20 in 26 seconds and found at least one email on 18 (35 addresses); the validator checked the 35 in 2 seconds and rated all 35 deliverable (18 were role addresses like info@). Cost: $0.04 for the websites (all 20 charged in the default mode, $0.036 with "requiredContact": "email") plus $0.0175 for the addresses, about $0.06. Well-known businesses publish more emails than average: across our 200-website benchmark an email was found on about 67% of websites. The free hasMx check on every email here already catches dead domains; the validator adds disposable, typo and no-reply flags, a verdict and score per address, and the ranked CSV with your business columns.
Fill the missing emails in a list you already have
Most lead lists (Google Maps exports, directories, CRM exports, places datasets) have a website for almost every business but an email for only some. This Actor fills the gaps and leaves everything else alone:
- Only rows without an email (
onlyMissingEmail: true): rows that already have an email are not read and not charged. - Your values are never overwritten: each row has a
fillobject with only the values for columns that are empty in your row (e.g.{"Email": "info@acme.de"}), ready to write back. - Every row keeps its identifier (
rowId: your sheet'srow_number, a Google MapsplaceId, an id column). Branches of a chain that share one website are read and charged once, but each branch gets its own row. - Send the rows of a sheet directly with
rows(from n8n, Make or the API), or pointdatasetIdat another run's dataset. The website, email, phone and id columns are found automatically (Website,Email,row_number...), or set them withdatasetField,emailField,phoneField,rowIdField. - No double payment across runs: with a
dedupeStoreName(any name), a website (by domain) read in an earlier run in the last 90 days is returned again withfromPreviousRun, without being read or charged. Failed websites are not remembered, so they are tried again. - Retry list: websites that failed for a reason that may pass next time (timeout, blocked, connection error) or were not started because your maximum charge was reached are listed in the key-value store record
RETRY; itsinputcan be used as the next run's input as is. Domains that no longer exist are not in it. - Each row has an
emailStatus:new_email,no_email_found,already_had_emailornot_read(witherrorType, andretryable: truewhen it is worth another try).
{"rows": [{ "row_number": 2, "Business": "Pizzeria A", "Website": "pizzeria-a.de", "Email": "" },{ "row_number": 3, "Business": "Dentist B", "Website": "dentist-b.com", "Email": "hello@dentist-b.com" }],"rowIdField": "row_number","onlyMissingEmail": true,"dedupeStoreName": "my-lead-sheet"}
Real test (October 2, 2026), counted per website, not per dataset row. Two lists from Places Database (Overture Maps): 200 Berlin businesses (restaurants, hotels, dentists, sent as rows like a Google Sheet) and 165 Lisbon dental clinics (by datasetId).
| Berlin, 200 rows | Lisbon, 165 rows | |
|---|---|---|
| Rows with a website | 199 | 151 |
| ... of which already had an email (skipped, free) | 62 | 19 |
| Rows with a website and no email | 137 | 132 |
| Websites read (chain branches merged) | 129 | 117 |
| Websites where a new email was found | 89 | 72 |
| Rows that got a new email (incl. branches) | 95 | 79 |
| Websites charged (email, phone or social profile found) | 97 | 85 |
| Websites not reachable (free) | 21 | 27 |
The Berlin list went from 62 to 157 rows with an email (+95). Cost: 97 × $0.002 = $0.19. Running the same sheet again with the same dedupe name reused 97 websites (not charged) and charged 1 newly found website.
Ready-made n8n workflows: Fill missing emails in a Google Sheet (reads the sheet, sends only rows without an email, writes the email and a status back to the same row, retries the retry: rows on the next run) and Add missing emails to a Google Maps or places dataset. Both are in the public n8n workflows folder, together with a third one that also verifies the emails and adds company data.
Switching from another contact scraper
Paste your existing input; it works as is. Inputs written for other popular contact scrapers are read without changes: startUrls ([{ "url": ... }] or plain strings), urls, websites and domains become the website list, maxRequestsPerStartUrl becomes Pages per website (up to 20), maxWebsitesConcurrency becomes Parallel websites, and proxyConfig or a useProxy proxy object becomes Proxy. The run log lists every field that was mapped, and names the settings that are not needed here (crawl depth, browser, retries, verification and enrichment add-ons) with the reason. Nothing in your input changes the price.
For example, this input from a page-based contact scraper starts a normal run here:
{"startUrls": [{ "url": "https://katzsdelicatessen.com" }, { "url": "https://hofbraeuhaus.de" }],"maxRequestsPerStartUrl": 20,"maxDepth": 2,"sameDomain": true,"mergeContacts": true,"proxyConfig": { "useApifyProxy": true }}
The log then shows "maxRequestsPerStartUrl": 20 is used as "Pages per website" = 20 and Not needed here: "maxDepth", "sameDomain", "mergeContacts".... The likely contact pages (contact, imprint, about, team...) are read first, so the default 8 pages are usually enough. With proxyConfig, Apify Proxy traffic is billed by Apify; most websites work without it.
How the two most used website contact scrapers compare (Free/Bronze plan prices from the Apify Store, checked October 2, 2026; check their Pricing tabs for changes):
| This Actor | vdrmota/contact-info-scraper | automation-lab/website-contact-finder | |
|---|---|---|---|
| Users in the last 30 days | new | about 2,380 | about 120 |
| Charged per | website with a contact found | page scraped | website scanned |
| Price | $0.002 per website | $0.002 per page | $0.001 per website |
| Start fee per run | none | $0.001 per GB of memory (4 GB default: $0.004) | $0.035 |
| 1,000 websites, 5 pages each | $2.00 at most | $10.00 + start fee | $1.035 |
| 1,000 websites, 20 pages each | $2.00 at most | up to $40 + start fee | $1.035 |
| 20 websites (a small run) | $0.04 at most | $0.20 + start fee | $0.055 |
| Website with nothing found | free | pages are charged | charged when the scan succeeds |
| Unreachable website | free | — | free |
| JavaScript-only websites | browser fallback, no extra charge | optional browser (+$0.003 per page) | no browser |
| Email checks | free: syntax, MX, role/personal, free mail, best email per site (no SMTP) | add-on (paid) | add-on (paid, MX/SMTP) |
| Named leads, social profile details | no | add-ons (paid) | no |
| Read websites from another run's dataset (e.g. Google Maps) | yes (datasetId) | no | no |
| One row per email (for outreach tools) | yes (outputMode) | one row per domain, or per page | one row per website |
In short: compared with a per-page scraper, this Actor costs the same per website as one page there, and websites with nothing found are free. A per-website scraper with a start fee is cheaper per website on large runs where most websites have contacts (above about 35 websites per run at Free plan prices); this Actor is cheaper on small runs and when many websites have no contacts, and it reads JavaScript-only websites with a browser at no extra charge. Every email gets free syntax and MX checks and a role/personal flag here; if you need SMTP-verified mailboxes or named leads, the paid add-ons of the other Actors offer them.
Use with AI agents (MCP)
Connect Apify's MCP server (https://mcp.apify.com?tools=tidytools/website-contact-extractor) to Claude, Cursor or any MCP client, then ask e.g. "Find the emails, phone numbers and social profiles of these 30 companies."
{ "domains": ["stumptowncoffee.com", "hofbraeuhaus.de"] }
Websites with nothing found and unreachable websites are not charged; failed rows carry an errorType.
Use it from code
curl -X POST "https://api.apify.com/v2/acts/tidytools~website-contact-extractor/run-sync-get-dataset-items?token=YOUR_TOKEN" \-H "Content-Type: application/json" \-d '{"domains":["stumptowncoffee.com"]}'
Send websites in domains (plain strings; bare domains work). The URL-list field startUrls is also accepted, but Apify rejects the whole run (HTTP 400) when it holds a bare domain or a blank line.
Need industry, a one-line description and a country guess as well? See Company Website Enrichment by tidytools.
Schedules and integrations: run it daily or weekly with Apify Schedules, get a webhook when a run finishes, or send the results to Zapier, Make, n8n, Google Sheets, Slack and other apps with Apify integrations. Results can be exported as JSON, CSV, Excel or XML.
Advanced settings
- Plain HTTP requests from: Auto (recommended) reads pages from Apify's network first, because the raw HTML is needed for Cloudflare-protected emails and contact forms, then from our servers, then with a real browser, and a website that refuses all of these is tried once more from our servers in other regions. If the home page is still refused with HTTP 403 or a refusal of automated clients (not a CAPTCHA or bot-check page), it is asked once more through our Cloudflare WARP network with a standard browser User-Agent that still ends with
TidyToolsContacts/1.0(inputwarpRetry, on by default; Auto and Our servers only); a website read this way is charged like any other, and one that refuses again or shows a bot check stays a freeblockedrow. JavaScript-only home pages are read with a browser automatically. A bare domain that does not answer is also tried as www. and, for a broken certificate, over http. A home page that only forwards visitors (<meta http-equiv="refresh">or a one-line script) is followed like a redirect. - Pages per website (default 8): the home page and the likely contact pages (contact, imprint/Impressum, about, locations, team, support, legal) are read first. When none of them shows an email and pages are left over (for example the home page links to no contact page), the remaining pages go to the pages most likely to name a mailbox: policies and terms, catering/events, referrals and patient forms, jobs, locations, FAQ, taken from the home page's links and then from the sitemap (
kind: "more"inpagesChecked). It stops at the first page with an email and never reads more than this setting; the price per website does not change. In a test on 65 small-business websites where the contact pages had no email, this found an email on 6 more of them. - Pages not to read (
excludePaths, optional, empty by default): paths such as/blogor/privacythat are never picked from links, guessed or taken from the sitemap; the next likely page takes their place in the page budget, and a picked page that redirects there is not used./blogalso covers/blog/...but not/blogger; letters are compared regardless of case and the query (?...) is ignored. The home page and pages you give yourself (acme.com/blog) are always read. Excluding pages can mean that fewer websites have a contact found, and only those are charged. An entry that is not a path starting with/(a full URL, a wildcard*, a query) gives one freeinvalid_inputrow and nothing is read. Example:"excludePaths": ["/blog", "/privacy"]. - How the crawler identifies itself: every request names us: the user agent is
TidyToolsContacts/1.0 (+https://apify.com/tidytools)from Apify's network, andTidyToolsSmartFetchfrom our servers and in the browser; only the one WARP retry of a refused home page (above) sends a browser User-Agent, still ending withTidyToolsContacts/1.0. robots.txt is not used to refuse a website you asked for: the sites you list are read even where their robots.txt disallows automated crawlers, as when you open them in your browser. Turn on Skip pages robots.txt disallows (respectRobotsTxt: true) to follow it: a page that the website's robots.txt disallows forTidyToolsContacts(or for aTidyToolsgroup, else for*) is then not read and is listed inrobotsDisallowedPages, and a website whose home page is disallowed returnserrorType: "robots_disallowed"and is not charged. - Time limit per website (default 120 seconds): after it, no further pages of that website are started and the pages read so far are returned (the skipped ones are listed in
timedOutPages). If even the home page could not be read in time, the website fails witherrorType: "timeout"(not charged). - Proxy: optional Apify Proxy for requests sent from Apify's network (billed to your Apify account).
Limitations
- Only public pages; nothing behind a login: a website that is a sign-in page (or redirects to one, or answers HTTP 401) returns
errorType: "login_required", is not charged and is never retried with credentials. Sites with strong bot protection can stay unreachable (not charged); a residential proxy may help. - Values are what the website shows. Phone numbers written without a country code are read with the site's likely country (address, international numbers on the site, domain ending, language), which can be wrong for multi-country sites.
- Email addresses shown only as images, or only inside a contact form, cannot be found. This is the main reason a website has no email in the output: in a 50-site sample (October 2026, 7 countries), 11 of the 19 websites without an email had only a contact form, and none of 7 small businesses in Osaka published an email (Japanese sites use inquiry forms).
- Store directories can list many numbers (e.g. one per branch); all of them are returned, each with its page.
- The Actor reads a handful of pages per site, not the whole site.
Responsible use
- The Actor reads only information that a company publishes on its own public website (business emails, phone numbers, social profiles, address). It does not guess, generate or verify personal email addresses, and it does not log in anywhere.
- Every request identifies itself as TidyTools (the one WARP retry of a refused home page uses a browser User-Agent that still ends with our TidyTools token). robots.txt is not used to refuse a website you asked for, unless you turn on Skip pages robots.txt disallows (
respectRobotsTxt). Pages behind a login are reported aslogin_requiredand never logged into, bot checks and CAPTCHAs are never solved, and when a site answers HTTP 429 with Retry-After we wait (up to 30 seconds) or leave the page for a later run. - You are responsible for how you use the results: follow the data-protection and anti-spam laws that apply to you and to the people you contact (for example GDPR in the EU/UK, CAN-SPAM in the US, CASL in Canada), and respect each website's terms.
- Business contact details can still be personal data (e.g. a named employee's email). Keep only what you need and honour opt-out requests.
- Want company-level data only? Turn on Role mailboxes only (no personal emails) (
roleEmailsOnly): only shared inboxes such as info@, sales@ or support@ are returned; addresses that look like a named person are dropped before output.
FAQ
Five quick answers for common run outcomes
Why company_not_found or invalid_input? A company name without a verified website is free; add its city, address or phone, or paste its website instead. For invalid_input, check that each line is a domain or URL; a pasted table needs a header row.
Why network, not_found or timeout? Check the domain in a browser and correct typos. not_found means the homepage answered 404/410, so use the current site address. A temporary connection failure or timeout may be in the run's RETRY record; DNS and certificate failures are not retried. These failed rows are free.
Why blocked, robots_disallowed or login_required? The site refused access, your respectRobotsTxt setting skipped a disallowed page, or the page requires sign-in. These rows are free. The Actor does not log in or solve bot checks; use a public business website. respectRobotsTxt is off by default and can be turned on.
Why PARTIAL_RESULTS or failed? Other sites in the run may have succeeded. Check SUMMARY.failedByErrorType, then the row's error and RETRY.input for temporary failures. A failed row is free; rerunning can still fail if the site itself is unavailable.
When am I not charged? With the default requiredContact:"any", a site is charged only if an email, phone or social profile is found. With requiredContact:"email", only sites with an email are charged. No contact, unreachable/blocked sites, invalid lines, unverified company names and duplicates are free; each row shows charged.
More details
Why do some websites have no email in the output? Most often the site publishes no address you can read: only a contact form, an address shown as an image, or a country where inquiry forms are the norm. In a 50-site sample (October 2026), 11 of the 19 websites without an email had only a contact form. A row with a phone or social profile can still be charged under the default requiredContact:"any"; choose requiredContact:"email" to pay only when an email was found. Rows still carry any phones, social profiles and contact-page link that were found. Run the site again with more Pages per website if its contact page is deeper.
Why was a website unreachable (errorType: "network")? The website could not be reached: the domain does not exist in DNS (a typo or an expired domain), the server does not answer or drops the connection, or its HTTPS certificate is invalid (certificate checks are never skipped). The row is free, and the run still finishes with the other results (SUMMARY status PARTIAL_RESULTS). What to do: open the domain in a browser and fix typos. Domains that do not exist in DNS are not retried; the other network rows are listed in the RETRY record, and a run with its input later often works.
Why does my large run finish as partial, and how do I re-run only the failed sites? On a list of hundreds or thousands of websites, a few nearly always cannot be read: unreachable (network), refused (blocked), behind a sign-in page (login_required), disallowed by robots.txt when respectRobotsTxt is on, or a company name without a verified website (company_not_found). The run then ends as PARTIAL_RESULTS; the other rows are complete, and failed rows are free. Temporary failures, including websites that ran out of their time limit (timeout), are already tried once more near the end of the run, with half as many websites in parallel and only while the run has time left (not DNS or certificate errors, and not 403); a website is still charged at most once. SUMMARY counts the failures by type (failedByErrorType). A re-run may still fail for the same sites.
How to re-run:
- Open the run's key-value store and the
RETRYrecord (byErrorTypeshows what failed). - Copy its
inputobject:RETRY.input.rowsholds only the failed sites, with your row IDs. - Start a new run with that JSON as input. For company names, add a city or country first.
Why does a row say errorType: "not_found"? The server answered, but the website's home page returned "not found" (HTTP 404 or 410), often a site that moved or a business that closed. The row is free and is not put in RETRY (the same address would fail again). What to do: open it in a browser; if the business now uses another domain, run that domain instead.
Why does a row say errorType: "failed"? The website could not be read for a reason that fits none of the other types; the row's error field has the details. The row is free and is listed in the RETRY record. What to do: run the RETRY record's input (it doubles the time limit per website, up to 900 seconds); if the same website fails again, check it in a browser or leave it out.
Why are some websites marked as refused? Some sites answer automated visitors with an error (403) or a bot check. The Actor identifies itself as TidyTools and never solves CAPTCHAs or bot checks; a plain 403 refusal gets one retry through our WARP network with a browser User-Agent (still ending with our TidyTools token), and a website that still refuses, or shows a bot check, is reported as refused (errorType: blocked) instead of being forced open (with respectRobotsTxt on, a home page robots.txt disallows is robots_disallowed; a sign-in page is login_required). These rows are free, and the run summary at the end of the run counts them (refused, blocked by robots.txt, behind a sign-in page, unreachable). Websites behind bot protection may work if you set the proxyConfiguration option (your own proxy choice and cost); the RETRY record lists the websites worth another try.
How does it find a company's website from its name? It checks, in order, our places database (Overture Maps), Wikidata's official-website property (P856, strict name match) and the likely domains, and uses a website only after verifying it on the site itself (name, and the row's city, street or phone when you give one). Parked or for-sale pages and guessed domains that redirect to an unrelated site are not used. A name without a verified website is a free company_not_found row with a hint on what to add.
Why do some rows have contacts from a places listing? When the website was found through its places listing (Overture Maps) and the website itself shows no email or phone, the listing's email or phone is added with source: "overture-places". These listing contacts are not charged: the charge depends only on what the website itself shows. SUMMARY withListingEmail counts these websites. Price unchanged: $2 per 1,000 websites where an email, phone or social profile was found; nothing found is free.
Does the Actor collect usage data? The Actor sends one anonymous usage counter per run (input type, item count range, outcome) to improve the tool; no URLs, content or account data.
Which pages does it read on each website? The home page plus likely contact pages: contact, imprint/Impressum, locations, about, team, support and legal pages, up to 8 pages by default.
Do I pay for websites where nothing is found? No. You pay only when at least one email, phone number or social profile is found. Unreachable websites, refused (blocked) websites, invalid lines, company names without a verified website (company_not_found) and duplicates are free too. With requiredContact: "email", a website with no email is free as well.
Can I give company names instead of websites? Yes. Put one business per line in Websites or company names, with the city or address after the name (Joe's Pizza, 7 Carmine St, New York, NY), or paste spreadsheet columns, tab-separated, ideally with a header row such as Name,City,Phone. Keep Find websites for company names on (default). The Actor looks up the official website, checks it belongs to that business, then reads its contacts.
Why did my company return company_not_found? No website could be verified for that name, so the row is free. Names alone are ambiguous: add the city, address or phone in the same line, or give the website directly. The row's hint says what to add. If you paste a table, include a header row so each column is understood. A find rate is not promised; see Company names instead of websites for our test.
The hint starts with | What it means | What to do |
|---|---|---|
the business seems to have no website of its own | The places data has this business, but no website | If it has one, add a website column |
the candidate website did not verify as this business | The website we found does not match the name or location | Check the name and location, or add its website address |
the candidate website could not be read | The website is blocked by robots.txt, returns an error or is not public | Check that it is publicly accessible, or add its website address |
the places listing has no usable website address | The places listing has no valid website address | Add its website address |
the city "<city>" was not recognised | The city was not understood | Write it as "City, CC" (country code), add a country column, or give the website |
the places lookup was unavailable | The places lookup failed this time | Retry later, or add the website address |
Tab-separated rows need a header row | Pasted tab-separated rows have no header row | Add a header row such as name |
check the spelling of the name | Nothing more specific applies | Check the spelling, or add the website |
Each row also has nameLookup.tried: the sources and domains that were tried, and what happened (for example "does not resolve").
How do I know the website found for a company name is the right one? Check websiteFromName.websiteVerifiedBy: places-site (the places listing's website names itself as the business), places-address (its home page gives no name, but the listing is at the row's city, street, postcode or phone), places-page (the page text names the business), site-name-location (a likely domain whose site name matches and whose page shows the row's city, street, postcode or phone), site-phone (the page shows the row's phone) or site-name (site name only, when the row gives no location). websiteConfidence is high when a second signal backs the name (the location or phone on the page, or a listing at the row's street, postcode or phone), otherwise medium. A domain that only contains the name is not accepted: the row is a free company_not_found.
What if the same company or website appears several times? The website is read and charged once. Later rows for the same website show charged: false and duplicateOf; with datasets the note says "result copied (charged once)".
Why do some websites have no email? Mostly because the business does not publish one. In our October 2026 test on 100 small-business websites from map listings (10 countries: plumbers, dentists, cafes, lawyers, shops...), 71 gave an email. Of the other 29: 11 offer only a contact form, 7 show no email on any page we could read (one-page sites, chain store pages, shops with phone and social links only); 11 could not be read at all: 3 have an invalid or expired HTTPS certificate (certificate checks are never skipped), 4 refuse automated visitors or answer with a server error, 2 home pages return "not found", 1 server does not answer, and 1 forbids crawling in robots.txt (respected at the time; since October 13, 2026 such a site is read unless respectRobotsTxt is on). Websites that cannot be read and websites with nothing found are not charged. A run with many websites therefore nearly always has a few "not reachable" rows: that is the websites, not the run. Japanese small businesses, in an earlier test, almost never publish an email (they use inquiry forms).
Can it add emails to Google Maps results? Yes, that is its main use. Pass the dataset ID of a Google Maps run (e.g. compass/crawler-google-places); the website field is found automatically and each row keeps the place's name, address, phone, Maps link and placeId. Source rows without a usable website are free and marked websiteStatus: "not_listed_in_source"; this does not prove the business has no official website. It does not scrape Google Maps itself.
Are the emails verified? Each email gets free checks: syntax, MX records (the domain receives mail), role or personal, free mail, and one bestEmail per website that skips domains without mail servers. No mail server is contacted (no SMTP check), so a mailbox can still bounce.
Can it find emails that are hidden from bots? Often, yes: obfuscated addresses such as "info [at] firm [dot] de" or "info@firm.com.tw", Cloudflare-protected emails and the common email-encoder scripts are decoded. Addresses shown only as images or only inside a contact form cannot be found.
What are the limits? Up to 50,000 websites per run and 1 to 20 pages per website (8 by default). Each website has a time limit (120 seconds by default, up to 900). Set a maximum charge per run to cap the cost: when the next website could exceed it, the run stops and lists the websites not processed.
How fast is it? Parallel websites is a global limit of 10 by default (up to 20). Websites under the same registrable domain (such as a.shop.com and b.shop.com) are read one at a time; websites on other domains can run in parallel. In our Google Maps test, 30 places took 12 seconds before this scheduling change. The one-site prefill limits reading to the homepage; our October 19 local extraction check took 4.7 seconds, but Apify run time can differ.
Is it legal to extract emails from websites? The Actor reads only public pages that a company publishes on its own website, never logs in, never solves CAPTCHAs and identifies itself as TidyTools on every request. robots.txt does not stop it from reading a website you asked for unless you turn on respectRobotsTxt (then disallowed pages are not read and a website whose home page is disallowed is skipped for free). Whether and how you may contact the people you find depends on the laws that apply to you (GDPR, CAN-SPAM, CASL...); see Responsible use. This is not legal advice.
Does it work with the API, webhooks, Make, n8n or Zapier? Yes. Start runs from the Apify API, a schedule, a webhook or the Apify apps for Make, n8n and Zapier, chain it after a Google Maps run with the Integrations tab, or use it from AI agents through MCP.
Support
Open an issue in the Issues tab with the website. Issues are checked regularly.
Found this useful? A short review on the Store helps other users find it.