Google Maps Email Extractor & Dead Website Checker
Pricing
from $1.60 / 1,000 business rows
Google Maps Email Extractor & Dead Website Checker
Search Google Maps by business type and city and get every business with its emails, phones, socials and booking links, plus a verdict on whether its website is alive, parked, for sale, redirected or dead. No browser, cheap per row.
Pricing
from $1.60 / 1,000 business rows
Rating
0.0
(0)
Developer
Northvane
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
11 days ago
Last modified
Categories
Share
Search Google Maps by business type and city, and get back every business with its emails, phones, socials and booking links – plus a verdict on whether its website is actually alive. No browser, so it is cheap and fast: 25 nail salons in San Antonio come back in about 13 seconds with 10 published email addresses, 7 businesses that have no website at all, and 2 whose domains now redirect somewhere else.
Directory listings outlive the businesses in them. In a real 82-company list read by hand, 11% of the websites were dead, parked, for sale, or serving gambling spam on a domain that used to belong to a dental clinic. Nothing upstream had removed them. This Actor is the step that removes them, in a few minutes, for a fraction of a cent per row.
How to run it
Put one or more search terms in Google Maps search terms – nail salon, roofing contractor, dentist – and a city in Location: San Antonio, TX, Manchester, UK, or any address Google Maps can resolve. Pick how many places per term (up to 1,000, paged from Google's own map endpoint). Run.
Three free filters run before anything is billed: only businesses without a website, minimum rating, maximum rating. Places the filters drop are never charged for.
Results stream into the dataset as they finish, in four views: Leads, Overview, Contacts (one row per email with provenance) and Dead & doubtful.
What you get per business
Every row carries the place under place: name, address, lat/lng, rating, category, phone (E.164), place ID, open state, Maps URL. Review counts and "permanently closed" are not in Google's list payload, so they are not claimed.
On businesses whose website is usable, the Actor reads the homepage and, if no address is published there, up to two contact-style pages the site itself links to (/contact, /about, /impressum, /kontakt, /mentions-legales…). It returns:
- Emails – from
mailto:links, visible text (including[at]/[dot]obfuscation), JSON-LD structured data and Cloudflare email-protection payloads, decoded. Every address carriesfoundOn(which page),source(mailto / text / jsonld / cf-decoded),region(footer / header / body),isRole(info@, hello@…),isFreemail(gmail, outlook…),domainMatch(address domain equals the website domain) and an MX check (mxValid,mxHost) so you know the domain can receive mail at all. primaryEmail– the best single address: same-domain, MX-valid, never a quarantined one.contactClass–EMAIL,FORM_ONLY(contact form or booking link but no address),NEITHER(site up, nothing stranger-facing), orNOT_VISIBLE_TO_FETCHER(JS-only site – not the same as "publishes no address").- Phones (from
tel:links first), socials (Facebook, Instagram, LinkedIn, X, YouTube, TikTok, Pinterest, Yelp, GitHub), booking links (Calendly, cal.com, Acuity, HubSpot Meetings, Booksy, Fresha, Vagaro, TidyCal…),hasContactForm, and tech signals (WordPress, Wix, Squarespace, Webflow, Shopify, GoDaddy builder, GoHighLevel…). pagesFetched– every URL requested for the row with its HTTP status. The audit trail, so a row is still explainable a week later.
What the website verdict means
For every business the Actor reports one of these statuses:
| Status | Meaning |
|---|---|
NO_WEBSITE | The business is on Google Maps but lists no website – the classic web-design / marketing lead. Phone comes from Maps. |
ALIVE | Site loads and serves real content. |
JS_ONLY | Site is up but the HTML needs JavaScript to render (React/Next/Webflow shells, bot walls). The business exists; contacts may be invisible to a plain fetch. |
PARKED | Registrar or hosting parking page (GoDaddy lander, Sedo, ParkingCrew, Namecheap…). |
FOR_SALE | Domain marketplace page ("this domain is for sale", HugeDomains, Dan, Afternic…). |
PLACEHOLDER | Host default page, "coming soon", "site not published", suspended account, default nginx/Apache/WordPress. |
EXPIRED | Domain or hosting expired notice. |
REDIRECTED_OFF_DOMAIN | Final URL lives on a different domain – the business moved or was acquired. Reported with the target. |
SUSPICIOUS_CONTENT | The domain now serves gambling / pharma / affiliate spam. A classic sign of an expired domain re-registered by someone else. |
DEAD | DNS not found, connection refused, timeout, 404/410 at the root, or the CDN says the origin is gone. |
ERROR | Transient failure (bare 5xx, TLS problem). Worth re-running these rows once. |
NO_URL | The input row had no usable website value. Never silently dropped. |
usable is true for ALIVE, JS_ONLY and NO_WEBSITE – the rows a buyer should keep. Everything else goes in the Dead & doubtful output view.
Three guards other extractors do not have
- Shared-widget quarantine. Directory and partner pages often render an "other partners" module that carries someone else's email. A naive "first email on the page" rule stamps that address onto a dozen unrelated companies. This Actor ignores related/recommended/partner/testimonial modules when attributing an address, and any address that turns up on three or more unrelated sites is quarantined – reported, flagged, never used as
primaryEmail. - Not visible ≠ not published. A React shell, a Cloudflare challenge, or a 403 does not mean the business publishes no contact. Those rows say
NOT_VISIBLE_TO_FETCHERso you can send them through a browser-based pass instead of writing them off. - Blank beats guessed. No country inferred from a TLD, no scheme invented for a malformed link, no address constructed from a name. If the site published nothing, the field is empty.
Input
| Field | Default | Notes |
|---|---|---|
searchTerms / location | – | Google Maps search. Location is resolved on Maps itself. |
maxPlacesPerSearch / zoom | 60 / 13 | Places per term; zoom 11 ≈ whole city, 15 ≈ neighbourhood. |
withoutWebsiteOnly / minRating / maxRating | off | Free filters applied before anything is billed. |
extractContacts | true | Off = pure alive/dead check. |
followContactPages / maxContactPagesPerSite | true / 2 | Second pass when the homepage publishes no address. |
checkEmailMx | true | MX lookup per email domain. |
maxConcurrency / timeoutSecs | 20 / 20 | Throughput and patience. |
maxItems | 0 (all) | Sample a list before running all of it. |
startUrls / datasetId | – | The other two ways to feed it – see below. |
urlField / nameField | website / title | Which dataset columns to read. |
passThroughFields | true | Keep the original row under sourceRow. |
proxyConfiguration | off | Only needed if many rows come back http_403_blocked. |
Output example
{"url": "https://www.example-dental.com/","name": "Example Dental","status": "ALIVE","usable": true,"contactClass": "EMAIL","primaryEmail": "hello@example-dental.com","emails": [{ "email": "hello@example-dental.com", "foundOn": "homepage", "source": "mailto", "region": "footer","isRole": true, "isFreemail": false, "domainMatch": true, "mxValid": true, "mxHost": "aspmx.l.google.com" }],"phones": ["+15125550100"],"socials": { "instagram": "https://www.instagram.com/exampledental" },"bookingLinks": ["https://www.zocdoc.com/practice/example-dental"],"hasContactForm": true,"techSignals": ["wordpress"],"pagesFetched": [{ "url": "https://www.example-dental.com/", "httpStatus": 200, "label": "homepage" }],"flags": [],"checkedAt": "2026-09-05T02:10:41.512Z","sourceRow": { "title": "Example Dental", "website": "example-dental.com", "phone": "(512) 555-0100" }}
A dead row looks like this:
{ "url": "https://fluxfortify.com/", "status": "PARKED", "usable": false, "statusReason": "matched window.location(?:.href)?\\s*=\\s*[\"']/lander", "contactClass": "DEAD" }
Other ways to feed it
The Maps search is the main path. Two others exist for lists you already have, and the output is identical:
- A plain list of websites. Paste URLs or bare domains into Websites to check. This is the standalone dead-website audit: no Maps search, just verdicts and contacts.
- A dataset from another Actor. Put the dataset ID of any Maps, Yelp, directory or company-register scraper run into Dataset from another Actor. Leave Website column name as
website(it also triesurlanddomain) and set Business name column totitleso the checker can flag pages that never mention the business. Every original row is passed through undersourceRow, so the output is your list, enriched – no join needed. To make it automatic, add this Actor as an Actor integration on the scraper's run: it starts whenever the scraper finishes and receives the dataset ID.
Via API or MCP:
const run = await client.actor('<your-username>/google-maps-email-extractor-website-checker').call({searchTerms: ['roofing contractor'],location: 'Denver, CO',maxPlacesPerSearch: 120,withoutWebsiteOnly: false,});const { items } = await client.dataset(run.defaultDatasetId).listItems();
The Actor is exposed through the Apify MCP server, so an AI agent can call it as a tool in a lead-generation pipeline: scrape → check → outreach.
Pricing
Pay per result: you are charged per business row in the output, nothing else – places dropped by your filters are free. A 1,000-row Maps export costs about the price of a coffee to clean, and typically returns 100–150 rows you should not have paid to contact. Speed is around 3 websites per second at the default concurrency, so 1,000 rows finish in 5–6 minutes.
FAQ
Is the MX check an email verification? No. It confirms the address's domain can receive mail, which catches dead domains and typos cheaply. It does not confirm the mailbox exists. Chain a dedicated verifier on the EMAIL rows if you need that.
Why is a site JS_ONLY when it looks fine in my browser? Your browser runs JavaScript; this Actor deliberately does not, which is what makes it cheap. The status tells you the business is there and the contacts need a browser-based pass.
Why did it find info@ but not the owner's personal address? The second pass stops at the first page that publishes any address, to keep cost down. Raise maxContactPagesPerSite if you want deeper coverage.
What about sites behind Cloudflare? Cloudflare-obfuscated addresses (data-cfemail) are decoded without a browser. Cloudflare challenge pages come back as JS_ONLY with an http_403_blocked flag.
Does it respect robots.txt? It fetches one to three public pages per site with a normal browser user-agent and does not crawl. Use it on lists you have the right to contact.
Will Google block it? Single searches from Apify's datacenter IPs work without a proxy today. For sustained high volume (thousands of places per run, many runs a day) turn on Apify residential proxy in proxyConfiguration; the cost is about $0.08 per 1,000 places. If a search is blocked the run logs it and still processes any URLs or dataset rows you gave it.
Related
Works best chained after a Google Maps scraper, a Yelp scraper, or any company-register Actor that returns a website column, and before an email-verification or outreach Actor. For brand-new UK companies rather than local businesses, use the UK Companies House New Companies + Contacts Finder by the same developer.