Google Maps Leads & Emails Scraper (Website Check) avatar

Google Maps Leads & Emails Scraper (Website Check)

Pricing

from $0.85 / 1,000 places

Go to Apify Store
Google Maps Leads & Emails Scraper (Website Check)

Google Maps Leads & Emails Scraper (Website Check)

Scrape Google Maps business leads with phone, address, website, rating, reviews and opening hours. Each site is checked (missing, dead, parked, no HTTPS, not mobile-ready) and scored with reasons, and optional business emails and socials come from its own site. No login. $1.50 per 1,000 places.

Pricing

from $0.85 / 1,000 places

Rating

0.0

(0)

Developer

locaihost data

locaihost data

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

0

Monthly active users

14 hours ago

Last modified

Share

Get local business leads from Google Maps, with each business's website checked and scored, so you can see at a glance who has no website or an outdated one: name, phone, address, rating, reviews, hours, a lead score with plain reasons and, if you want them, business emails and socials. One row per business, no login.

  • Find web-design clients: list the plumbers, dentists or cafés in a town whose site is missing, dead, parked, not secure or not mobile-ready.
  • Prospect for SEO or IT work: score the same businesses for missing titles, slow pages, few reviews or a missing DMARC record instead.
  • Build a calling list: phone numbers, addresses, ratings and opening hours for every business a Google Maps search returns.
  • Add contact details: shared business mailboxes (info@, bookings@) and social profiles from each business's own website.
  • Never pay twice: list the places you already have in excludePlaceIds, or turn on skipPreviouslyReturned, and they are skipped and not charged.
  • Watch for new businesses: schedule a search with onlyNew and get only the businesses that appeared since the last run.

Quick start

Paste this into the JSON tab of the input and click Start:

{"searchTerms": ["dentist"], "locations": ["Austin, TX"]}

You get 10 businesses with their websites checked, usually in about 15 seconds. Every term is searched in every location; raise maxPlacesPerQuery (default 10, up to 200) for longer lists.

websiteFilter: "problem" returns only businesses whose site is missing, social-only, dead, parked, broken, or has no HTTPS: the ones worth a call. Add it to the input above; businesses it leaves out cost nothing.

What data can I get?

GroupFields
Businessname, category, categories, phone, phoneInternational, website, websiteDomain, rating, reviewCount
Locationaddress, addressLines, neighborhood, city, countryCode, lat, lng, timezone
HoursopeningHours (each day), openingHoursComplete, openStatusText, permanentlyClosed
Google IDsplaceId, cid, featureId, kgId, placeUrl
Website checkwebsiteStatus, websiteFinalUrl, websiteHttpStatus, websiteHttps, websiteMobileReady, websiteResponseMs, websiteCms, websiteCopyrightYear, websiteTitle, websiteHasMetaDescription, sslDaysToExpiry
Email set-upemailProvider, hasDmarc, dmarcPolicy
Lead scoreleadScore (0-100), leadBand (hot, warm, cool, skip), leadReasons
Contact details (optional)emails, emailSource, socials, contactPage
Run contextsearchTerm, searchLocation, searchLocationResolved, placeInput, sampleHit, scrapedAt

One real row (run on 2026-10-18 with the quick-start input, shortened):

{
"name": "Tech Ridge Dental",
"category": "Dentist",
"address": "12400 N Interstate Hwy 35 Ste A131, Austin, TX 78753",
"city": "Austin, TX",
"countryCode": "US",
"lat": 30.4043561,
"lng": -97.6759803,
"phone": "(512) 821-2394",
"phoneInternational": "+1 512-821-2394",
"website": "http://www.techridgedental.com/",
"rating": 5,
"reviewCount": 1187,
"openStatusText": "Closed · Opens 7:30 AM Mon",
"placeId": "ChIJg6Z3MsjORIYRZh2Pk4RntBw",
"websiteStatus": "ok",
"websiteHttps": true,
"websiteResponseMs": 301,
"websiteMobileReady": true,
"websiteCopyrightYear": 2011,
"sslDaysToExpiry": 34,
"emailProvider": "Other",
"hasDmarc": false,
"leadScore": 15,
"leadBand": "cool",
"leadReasons": ["Footer copyright still says 2011."]
}

The dataset has three views (Leads, Contacts, Listing details) and exports to JSON, CSV, Excel or HTML. The run's SUMMARY record counts what was found, filtered and charged.

Website statuses

websiteStatus is one of none, social-only, booking-page, unreachable, ssl-invalid, parked, broken, insecure, blocked, timeout or ok (unchecked when website checks are off). The less obvious ones:

  • social-only: the listing's website is a Facebook, Instagram or link-in-bio page (Linktree and similar), or the domain just forwards to one.
  • booking-page: the listing's website is a page on a booking or scheduling platform (Calendly, Acuity, Zocdoc, NexHealth, Dentally, Fresha, Booksy, Vagaro, Mindbody, Jane, Cliniko, Square Appointments, OpenTable and others), or a bare booking app on the business's own domain. It works, so it is not a "problem", but the business may have no site of its own.
  • parked: real evidence of a placeholder: a parking service or domain marketplace (GoDaddy, Sedo, Dan.com, Afternic, HugeDomains, Bodis and others), a "domain for sale" notice, or a registrar's or web host's default page. A small or script-only page is never called parked on size alone.
  • blocked / timeout: the site refused or outlasted the automated check. That says nothing about the business, so these are never scored as problems.

Score leads for what you sell

The same business is a different lead depending on who you are, so leadProfile picks what counts as a reason to call:

ProfileTreats as a reason to call
Web design (default)No website, dead domain, parked page, broken site, invalid certificate, social page or booking page instead of a site, no HTTPS, not mobile-ready, stale copyright, slow
SEOMissing <title>, no meta description, not mobile-ready, slow homepage, few reviews, low rating
Email set-up / ITNo DMARC, DMARC set to p=none, certificate expiring, domain expiring, no HTTPS
Local marketingNo opening hours, few reviews, no phone, no website, low rating

The reasons in leadReasons are plain sentences you can paste straight into your own outreach.

Contact details

Turn on includeContacts (website checks must be on too) and each row also gets the contact details the business publishes on its own website. They are read from the homepage the website check already fetched, plus at most two more pages on the same site: the contact page, then the about page, both found from the homepage's links, and only if the site's robots.txt allows it.

{
"searchTerms": ["dentist"],
"locations": ["Austin, TX"],
"maxPlacesPerQuery": 50,
"checkWebsites": true,
"includeContacts": true
}
  • emails: shared business mailboxes only, up to 5: info@, hello@, sales@, bookings@, or an address named after the business itself. Anything that looks like a person's address is dropped: firstname@, firstname.lastname@, and personal Gmail, Outlook or Yahoo addresses. Addresses hidden by Cloudflare's email protection are decoded.
  • emailSource: the page the first email was found on.
  • socials: the business's Facebook, Instagram, LinkedIn company page, X, TikTok and YouTube (null where there is none). Personal profiles, share buttons and a website builder's own accounts are left out. A listing whose website is a Facebook or Instagram page gets that page here too.
  • contactPage: the contact page's URL, when the homepage links to one.
  • emailDomainHasMx: true when an email was found on the business's own domain and that domain's public DNS lists mail servers (MX records), as seen during the website check. null means not known, never "invalid": no mailbox is probed and no mail is sent. Only present when includeContacts is on.

The Contacts view puts these first. An example (names and addresses made up):

name Acme Dental Care
phone (512) 555-0134
website https://www.acmedentalcare.example/
emails ["info@acmedentalcare.example", "appointments@acmedentalcare.example"]
socials {"facebook": "https://www.facebook.com/acmedentalcare",
"instagram": "https://www.instagram.com/acmedentalcare",
"linkedin": null, "x": null, "tiktok": null, "youtube": null}
contactPage https://www.acmedentalcare.example/contact/
emailSource https://www.acmedentalcare.example/

On a test of 55 real small-business websites (dentists, plumbers and cafés, October 2026), 71% gave at least one email or social profile and 42% gave an email. Sites that block automated visits, or build their page with JavaScript only, give nothing; those rows come back with empty contact columns and are not charged for contact details.

Never pay twice for the same business

Businesses you already have are dropped before any website is checked, so they are never returned and never charged. Two ways, which can be combined:

  • A list: put the placeId or cid column of an earlier export in excludePlaceIds, or Google Maps place links in excludeUrls. Place IDs, CIDs, feature IDs and ?cid= links all work. Short maps.app.goo.gl links are not read here (they would need a lookup), so use place IDs for those.
  • A memory: set skipPreviouslyReturned to true. The Actor remembers every business a run returns under memoryName and skips them in later runs, even for different searches or in different tasks that share that name. Use one name per customer or campaign.
{
"searchTerms": ["dentist"],
"locations": ["Austin, TX"],
"excludePlaceIds": ["ChIJO0xN0RG1RIYRIDFDZYJkYjA", "3105346571632226552"],
"skipPreviouslyReturned": true,
"memoryName": "austin-dental-2026"
}

SUMMARY.skippedExcluded and SUMMARY.skippedPreviouslyReturned count what was left out. Unlike onlyNew, which remembers one saved task's own searches, the memory name is yours to share. Price-first sample runs ignore the memory but still honour your exclude list.

Ratings and review counts were already in every row (rating, reviewCount). Google sometimes sends a shorter listing without the review count; those rows show null rather than a guess.

Look up businesses you already know

Put Google place IDs (ChIJ…), CIDs (the number in a ?cid= link) or feature IDs in placeIds, or Google Maps place links (copied from the address bar or the Share button, including maps.app.goo.gl short links) in startUrls. Each business is looked up directly, then checked, scored and charged exactly like a search result.

{
"placeIds": ["ChIJO0xN0RG1RIYRIDFDZYJkYjA"],
"startUrls": [{ "url": "https://www.google.com/maps/place/?q=place_id:ChIJO0xN0RG1RIYRIDFDZYJkYjA" }],
"leadProfile": "web-design"
}

These rows have no search, so searchTerm and searchLocation are null and placeInput holds the ID or link as you typed it. An ID Google has no place for is skipped, not charged, and listed in SUMMARY.placesNotFound. Search links (google.com/maps/search/…) are not read: put the words in searchTerms and the area in locations. Place IDs and a search can share a run; a business both find is returned once.

Reviews (optional)

Set maxReviewsPerPlace above 0 and each business also gets up to that many Google reviews, one row each (type: "review"): stars, full text, exact date, the owner's reply, likes, language, Google's translation into your language and whether the reviewer is a Local Guide. Its own row (now type: "place") gains a review summary: ratingDistribution (1 to 5 star counts over all its reviews), reviewTopics (the topics Google picked out, with mention counts), ownerReplyRate, avgReplyLagDays and ratingLast90d.

{
"searchTerms": ["dentist"],
"locations": ["Austin, TX"],
"maxReviewsPerPlace": 20,
"reviewsSort": "newest",
"reviewsSince": "3 months"
}

reviewsSort is newest (default), most-relevant, highest or lowest; reviewsSince takes a date or a span like 30 days; reviewRatings keeps only some star ratings, e.g. ["1", "2"]. Reviewer names are left out unless you set includeReviewerNames: true, and never come with profile links or photos. With onlyNew on, a business already returned is not returned again but its new reviews are. Reviews are the ones Google lists to a visitor who is not signed in. Reply dates are Google's rounded wording ("3 months ago"), so avgReplyLagDays is an estimate. With maxReviewsPerPlace at 0 (the default) nothing is read and every row is exactly as before. Only after the reviews? Google Maps Reviews Scraper is this same tool set up for reviews.

Filter before anything is charged

  • minRating: leave out businesses rated below this many stars (e.g. 4.5). Businesses with no rating yet are left out too.
  • categoryFilter: keep only businesses whose Google category contains one of your words. ["dentist"] keeps "Dentist" and "Cosmetic dentist" and drops the "Dental clinic" and the pharmacy that Google also returns for a dentist search.
  • skipClosed (on by default): permanently closed businesses are left out.

These read only the Google listing, so they run before any website is checked: a business they drop costs nothing. The status message and SUMMARY say how many each one left out.

How much does it cost?

You pay per business returned, plus per website actually checked, plus contact details only if you turn them on. There is no charge for Apify platform usage on top.

EventFree and StarterScaleBusiness
place: a business returned$1.50 per 1,000$1.10 per 1,000$0.85 per 1,000
website-check: a business's website actually fetched and checked$2.00 per 1,000$1.50 per 1,000$1.10 per 1,000
contact-details — only with includeContacts, and only when the business's own website gave at least one email or social profile$1.50 per 1,000$1.125 per 1,000$0.825 per 1,000
review: one review row, only with maxReviewsPerPlace above 0$0.30 per 1,000$0.225 per 1,000$0.165 per 1,000

Plus $0.00005 per run start. Apify's free $5 monthly credit covers about 3,300 businesses with website checks off. Free-plan runs stop at 500 businesses each; any paid Apify plan removes the cap.

What's free: businesses with no website or only a social page are never charged a website check; businesses dropped by websiteFilter, minRating, categoryFilter or skipClosed; duplicates across overlapping searches in one run; businesses skipped by onlyNew; reviews skipped by onlyNew, reviewsSince or reviewRatings; place IDs Google has no listing for; and contact details when a site gives none. Turn checkWebsites off and you pay only the place price. To cap a run's spend, set Maximum cost per run in the run options: the run stores only rows it can pay for in full (place, check and contact details together), then stops cleanly.

Price first: count, sample, then the full list

Building a list with an AI agent, or want the bill before you pay it? Run it in three steps. Each run's status message states what the next step will cost, from the run's own pricing, and gives the exact input for it (a very long search list is too big for a status message, so it then points to the run's SUMMARY record, where priceFirst always holds the full figures and the input).

1. Count. An ordinary places-only run, charged per business returned. It says how many businesses matched, how many have a website worth checking, what checking all of those would cost, and the input for step 2.

{
"searchTerms": ["plumber"],
"locations": ["Leeds, UK"],
"maxPlacesPerQuery": 100,
"countryCode": "gb",
"checkWebsites": false
}

2. Sample. Checks a random 50 of those websites (websiteCheckSample, 10 to 500) and returns only those 50 businesses, each charged one place and one website check, so you know its price before it runs. It reports how many of the 50 are hits, the estimated number of hits in the whole list with a 95% range, and what the full run would cost. The sample is random, never the first 50 in Google's order. The same search always picks the same 50; set sampleSeed to any word or number to draw a different, repeatable sample. Every row carries sampleHit.

{
"searchTerms": ["plumber"],
"locations": ["Leeds, UK"],
"maxPlacesPerQuery": 100,
"countryCode": "gb",
"checkWebsites": true,
"websiteCheckSample": 50,
"leadProfile": "web-design",
"websiteFilter": "problem"
}

3. Full list. The same input without the sample.

{
"searchTerms": ["plumber"],
"locations": ["Leeds, UK"],
"maxPlacesPerQuery": 100,
"countryCode": "gb",
"checkWebsites": true,
"leadProfile": "web-design",
"websiteFilter": "problem"
}

Want contact details in the final list? Add "includeContacts": true to all three inputs; each step's next input keeps the setting. A hit is a business that passes your websiteFilter (with "any", one scored hot or warm). Businesses with no website, or only a social page, are counted exactly rather than sampled, and a sample run doesn't return them. A sample run never reads or updates the onlyNew memory, so leave onlyNew off for these three steps.

Limits, stated up front

  • Google caps one search at roughly 120 results per location. To cover a large city, split it into several smaller locations rather than raising maxPlacesPerQuery.
  • We say when we could not check. If a site's firewall blocks automated requests it comes back as blocked, never as "broken", and nothing read from the firewall's page is reported or scored. Facts that don't come from the page still count: DMARC and email provider (from DNS) and the Google listing.
  • A timeout is not a fault. Slow small-business sites are common, so timeouts are reported and not scored.
  • Each website check reads the homepage once. Contact details read at most two more pages per site.
  • Ratings, review counts and opening hours are Google's at the time of the run. Opening hours are occasionally partial; openingHoursComplete says so.
  • Free-plan runs stop at 500 businesses (and 2,000 reviews when maxReviewsPerPlace is on). If a run nears its time limit, it stops cleanly, keeps every row found so far and says so in the status message.

Only new since last run, and schedules

Turn on onlyNew and the Actor remembers the businesses it already returned for the same searches, so a daily or weekly schedule becomes a new-business radar: each run returns only businesses that have appeared since the last one, and skipped businesses cost nothing. Leave it off for the first run to get everything. Set up a schedule under Schedules in Apify Console.

Integrations: API, n8n, Make and AI agents

API. Run it and get the rows back in one call:

curl -X POST "https://api.apify.com/v2/acts/locaihost~google-maps-leads/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"searchTerms": ["plumber"], "locations": ["Leeds, UK"], "countryCode": "gb", "websiteFilter": "problem"}'

Apify's API clients for Python and JavaScript work the same way.

n8n, Make, Zapier, Google Sheets, Slack, webhooks. Apify's built-in integrations send each run's dataset wherever you need it, for example straight into a CRM or a sheet your sales team works from.

AI agents (MCP). Add this Actor to Claude, Cursor or any MCP client with https://mcp.apify.com/?tools=locaihost/google-maps-leads, then ask something like "Find 50 electricians in Manchester whose website is missing or not mobile-ready, with their phone numbers." The price-first steps above give an agent the cost before it commits.

FAQ

How much does it cost to scrape Google Maps leads?

$1.50 per 1,000 businesses on the Free and Starter plans, plus $2.00 per 1,000 websites actually checked, less on Scale and Business. Contact details are an optional extra, charged only when found. Apify's free $5 monthly credit covers about 3,300 businesses with website checks off.

Can I use it through the API or from Python?

Yes. Use the run-sync-get-dataset-items call above, or Apify's Python and JavaScript clients. Every run's data is also available as JSON, CSV or Excel from its dataset.

Can I use it from an AI agent?

Yes. Through the Apify MCP server (https://mcp.apify.com/?tools=locaihost/google-maps-leads) any MCP client can run a search and read the rows back.

This Actor reads what any logged-out visitor sees on Google Maps and on each business's own public website. It never signs in, never uses an account or session cookie, and never touches anything behind a login. Google's terms restrict automated access, so decide for yourself whether your use fits them. It returns businesses, together with the contact details those businesses chose to publish. With includeContacts, the emails and social profiles come only from the business's own website, and only shared business mailboxes and business pages are kept, never a person's personal address. Some listings, sole traders especially, show a person's name, a home address or a mobile number as the business's contact details, which may still be personal data where you are. You are the data controller for what you do with the output. For marketing, the usual rules apply (GDPR/UK GDPR and PECR in Europe, CAN-SPAM in the US, and local do-not-call registers for phone numbers): check before you call or email, and honour opt-outs.

Why are some fields empty?

Many small businesses publish no website, opening hours or rating, and those fields stay null. Website fields are empty when there is no site to check, when checks are off (unchecked) or when the site blocked the check (blocked). Email set-up fields need DNS records the domain may not have. Contact details are empty when the business's site publishes none, or builds its page with JavaScript only.

Why did fewer businesses come back than I asked for?

Google returns at most about 120 results for one search in one place, and the filters (websiteFilter, minRating, categoryFilter, skipClosed, onlyNew) drop businesses before they are returned. The status message and SUMMARY say how many each filter left out.

More from locaihost data

Did it help?

If this saved you time, a quick rating on the Store helps other people find it.

Support

Something not working, or a field you need? Open an issue on the Issues tab: we answer issues within 24 hours.