Google Maps Email & Social Enricher
Pricing
from $2.40 / 1,000 lead-results
Google Maps Email & Social Enricher
Find emails and social profiles for businesses on Google Maps. Enriches each listing with up to four social profiles (LinkedIn, Facebook, Instagram, Twitter) and emails surfaced from the business website's home, contact, and about pages. Pay-per-lead pricing, CSV-friendly flat output.
Pricing
from $2.40 / 1,000 lead-results
Rating
0.0
(0)
Developer
Delowar Munna
Maintained by CommunityActor stats
0
Bookmarked
14
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Share
Google Maps Email & Social Media Extractor

Google Maps Email & Social Media Extractor turns a Google Maps search into a ready-to-send outreach list. Enter what you're looking for ("dentists") and where ("Manchester, UK"), and for every business the Actor visits its website to collect email addresses, phone numbers and LinkedIn, Facebook, Instagram, X, YouTube and TikTok profiles — alongside rating, reviews, address and coordinates from Google Maps. Every row is flat and CSV-ready for Google Sheets, HubSpot or your cold-email tool. Businesses we couldn't enrich at all are delivered free — no add-on fees, no residential proxy costs, no per-filter charges.
✨ What you get
- 30 flat fields per business — Maps data (name, category, rating, reviews, open/closed, phone, address, city, postcode, country, coordinates, Maps URL) plus website enrichment (best email, all emails, email count, website phone numbers, contact-page URL, and six social profiles).
- Search many areas at once — cross any list of queries with any list of locations. This is how you get past Google's ~120-results-per-search ceiling.
- Lead filters that cost you nothing — minimum rating, skip closed businesses, with/without a website, contacts-only. Filtered-out businesses are never saved, never charged, and don't count toward your result limit.
- Empty rows are free — one flat
lead-resultevent per row that carries a website, an email, or a social profile. A business with none of those is still delivered, and never billed. Note this means a listing whose only contact point is a website is charged, even when its site yields no email — roughly a third of rows in our own test runs. - No hidden costs — no per-filter fees, no add-on events, no platform-usage pass-through, no residential proxy bill.
- Genuinely flat output — one row = one business = one CRM record. No nested arrays or objects. Missing values are
"", nevernull. - Cleaned, not raw — website URLs stripped of
utm_*/gclid/fbclid, social links canonicalised to one profile per platform, junk addresses (noreply@,logo@2x.png) filtered out, Cloudflare-hidden addresses decoded.
🎯 Use cases
Cold email to local businesses. Run "plumbers" across five boroughs, export the Contacts view, and drop it into Instantly, Lemlist or Smartlead. primaryEmail gives you one best address per business, so there's nothing to de-duplicate by hand.
Social-DM outreach. LinkedIn, Facebook, Instagram, X, YouTube and TikTok sit in six plain columns, one profile per platform. Filter to rows where instagram is non-empty and you have a DM list — no parsing, no de-nesting.
Local-SEO and web-design prospecting. Set Website → Only without a website to get exactly the businesses with no site at all, then sort by rating and reviewsCount to find the ones with real customer demand and no digital presence. That shortlist is the pitch list.
Directory and lead-list building. placeId, latitude / longitude, searchQuery and searchLocation make every row traceable and mappable, so repeated runs merge cleanly into an existing database instead of duplicating it.
🚀 How to use it
- Enter one or more search queries ("dentists", "plumbers") and one or more locations ("Manchester, UK"). Every query is run in every location.
- Set Max results, and any Lead filters you want.
- Click Start. Rows appear as they're enriched; export as CSV, JSON or Excel, or pull them via the API.
Example 1 — one niche across several areas
{"queries": ["plumbers"],"locations": ["Brooklyn, NY", "Queens, NY", "Bronx, NY"],"maxResults": 300,"proxyConfiguration": { "useApifyProxy": true }}
Example 2 — qualified leads only
{"queries": ["dentists", "orthodontists"],"locations": ["Manchester, UK", "Leeds, UK"],"maxResults": 500,"onlyWithContacts": true,"minRating": "4","skipClosedPlaces": true,"websiteFilter": "withWebsite","language": "en","countryCode": "gb"}
Example 3 — businesses with no website (agency prospecting)
{"queries": ["hair salons", "barbers"],"locations": ["Austin, TX"],"websiteFilter": "withoutWebsite","minRating": "4","maxResults": 200}
Example 4 — direct Maps URLs
{"startUrls": ["https://www.google.com/maps/place/?q=place_id:ChIJN1t_tDeuEmsRUoP3ySjGub4","https://www.google.com/maps/search/dentists+in+melbourne+vic"],"maxResults": 100}
📦 Output
Two dataset views:
- Business leads — all 30 fields.
- Contacts — mail-merge-ready: business, best email, all emails, email count, both phone columns, website, the six socials and the contact page.
Every row has the same shape; anything not found comes back as "".

Fields (30)
| Field | Type | Source | Notes |
|---|---|---|---|
placeId | string | Google Maps | Stable Google identifier; the de-duplication key. |
title | string | Google Maps | Business name. |
category | string | Google Maps | Primary category, e.g. "Marketing agency". |
rating | number | Google Maps | Star rating, "" when the business has none. |
reviewsCount | number | Google Maps | Number of Google reviews. Best-effort: Google omits it on some listings. |
permanentlyClosed | boolean | Google Maps | true when Google marks the business permanently or temporarily closed. |
phone | string | Google Maps | As Google displays it. |
website | string | Google Maps | Tracking parameters (utm_*, gclid, fbclid, …) stripped. |
primaryEmail | string | Website | The single best address: own domain first, then a named person over a department. |
emails | string | Website | Comma-separated. Junk and asset-filename addresses filtered out. |
emailCount | number | Website | How many addresses are in emails. |
websitePhones | string | Website | Numbers published as tel: links, minus the one Maps already gave you. |
contactPageUrl | string | Website | The business's contact page — where to go when they publish a form, not an email. |
linkedin | string | Website | One canonical profile URL per platform; share / intent links dropped. |
facebook | string | Website | |
instagram | string | Website | |
twitter | string | Website | Twitter / X. |
youtube | string | Website | The channel, not a video link. |
tiktok | string | Website | The profile, not a single clip. |
address | string | Google Maps | See Limitations — search rows carry a street-level fragment. |
city | string | Google Maps | Usually blank on search rows — see the note under the samples. |
zipCode | string | Google Maps | Same: present on place-URL rows, blank on most search rows. |
countryCode | string | Google Maps | Two-letter code. Needs a full address or the Country input. |
latitude | number | Google Maps | From the listing's Maps URL. |
longitude | number | Google Maps | |
mapUrl | string | Google Maps | Direct link to the listing. |
searchQuery | string | Your input | The query (or start URL) that produced this row. |
searchLocation | string | Your input | The location it was crossed with — blank when you didn't use Locations. |
scraped_at | string | Run | ISO-8601 timestamp. |
is_enriched | boolean | Run | true when the row has a website, an email or a social profile. Charged rows. |
Sample records
Both are real rows from real runs, not illustrations.
From a search query
{"placeId": "0x487604cfb336698f:0xa2f0c34a77caff33","title": "Pearl Lemon","category": "Internet marketing service","rating": 4.8,"reviewsCount": 156,"permanentlyClosed": false,"phone": "+44 20 7183 3436","website": "https://pearllemon.com/","primaryEmail": "pm@pearllemon.com","emails": "info@pearllemongroup.com, dee@pearllemon.co, pm@pearllemon.com","emailCount": 3,"websitePhones": "+447454539583, +16502784421, +16137205715","contactPageUrl": "https://pearllemon.com/contact-us/","linkedin": "https://linkedin.com/company/pearl-lemon-company","facebook": "https://facebook.com/pearllemonmarketing","instagram": "https://instagram.com/pearllemongroup","twitter": "https://x.com/pearllemongroup","youtube": "https://youtube.com/@PearlLemonOfficial","tiktok": "https://tiktok.com/@pearllemon","address": ", Kemp House, 152 - 160 City Rd","city": "","zipCode": "","countryCode": "","latitude": 51.5273851,"longitude": -0.0891169,"mapUrl": "https://www.google.com/maps/place/Pearl+Lemon/data=!4m7!3m6!1s0x487604cfb336698f:0xa2f0c34a77caff33","searchQuery": "Digital Marketing Agency London","searchLocation": "","scraped_at": "2026-09-22T23:39:20.622Z","is_enriched": true}
From a Google Maps place URL
{"placeId": "0x48761cd0645b826b:0x31bb39a98980dbcf","title": "Make Agency","category": "Marketing agency","rating": 4.9,"reviewsCount": 75,"permanentlyClosed": false,"phone": "+442035670860","website": "http://makeagency.co.uk/","primaryEmail": "","emails": "","emailCount": 0,"websitePhones": "","contactPageUrl": "","linkedin": "","facebook": "","instagram": "","twitter": "","youtube": "","tiktok": "","address": "Cordy House, Curtain Rd, Shoreditch, London EC2A 3BS, United Kingdom","city": "London","zipCode": "EC2A 3BS","countryCode": "GB","latitude": 51.5252885,"longitude": -0.0806038,"mapUrl": "https://www.google.com/maps/place/Make+Agency/data=!4m7!3m6!1s0x48761cd0645b826b:0x31bb39a98980dbcf","searchQuery": "https://www.google.com/maps/place/Make+Agency/...","searchLocation": "","scraped_at": "2026-09-23T04:30:31.429Z","is_enriched": true}
These two rows show the trade-off honestly. The search row is rich in contacts — three emails, six socials, three extra phone numbers — but its
addressis a street fragment, socity,zipCodeandcountryCodeare blank. The place-URL row is the reverse: a complete, parsed address, but its website refused our fetcher, so every contact field is empty.That is the shape of the Actor. Keyword searches optimise for contacts; place URLs optimise for address detail. Use Locations when you need the area recorded —
searchLocationcaptures it regardless of what the address says.
What to expect in practice
Measured across a 50-row search run and a 4-row place-URL run:
| Field | Search rows | Place-URL rows |
|---|---|---|
title, category, rating, website, mapUrl, coordinates | 100% | 100% |
phone | 94% | 100% |
reviewsCount | 100% | varies — Google does not always publish a count |
primaryEmail / emails | ~52% | depends entirely on the site |
contactPageUrl | ~58% | — |
linkedin / instagram / facebook | 42–48% | — |
twitter / youtube / tiktok | 10–36% | — |
websitePhones | ~12% | — |
city / zipCode / countryCode | 0–2% | 100% |
Contact coverage tracks how much a business publishes and whether its site lets us read it — see Limitations. Treat these as indicative rather than a guarantee: they come from one vertical in one city.
💵 Pricing & cost examples
Pay-Per-Event — one result event, plus Apify's standard actor-start fee. No add-on events, no per-filter fees:
| Event | Charged when |
|---|---|
lead-result | Once per business saved to the dataset that carries at least one of: website, emails, or any social profile URL. |
Your bill is charged rows × price per event, plus Apify's small per-run start fee.
How much will it cost?
At the current price of $4.00 / 1,000 events on the Free plan and $2.40 / 1,000 on Gold, assuming ~80% of listings return at least one contact point:
| Leads you want | Listings processed | Free plan | Gold plan |
|---|---|---|---|
| 100 | ~125 | ~$0.40 + $0.04 | ~$0.24 + $0.04 |
| 1,000 | ~1,250 | ~$4.00 + $0.04 | ~$2.40 + $0.04 |
| 5,000 | ~6,250 | ~$20.00 + $0.04 | ~$12.00 + $0.04 |
The trailing figure is the one-off actor start fee ($0.02 per GB of run memory; the 2 GB default puts it at about $0.04 per run, whatever the run's size). Check the Pricing tab for the live per-event price on your plan.
Not charged
- Rows with no website, no email and no social link — delivered for completeness, marked
is_enriched: false, never billed. - Anything removed by a filter — minimum rating, closed businesses, website filter, contacts-only. Filtered businesses are never saved and never charged, and they don't consume your Max results either.
- Duplicates removed by Google Place ID.
- Rows with no business name (dropped).
- Failed dataset pushes, and failed or empty website fetches.
- Anything after your per-run spending limit is reached — the Actor honours it and stops charging cleanly.
⚙️ Input reference
| Field | Type | Default | Purpose |
|---|---|---|---|
queries | Array | [] | What to search for, one per line. |
locations | Array | [] | Areas to search in. Every query runs in every location. |
startUrls | Array | [] | Google Maps place URLs and/or search URLs. |
| Result limits | |||
maxResults | Integer | 50 | Run-wide cap on businesses saved (hard ceiling 5000). |
maxResultsPerSearch | Integer | 120 | Cap for each query × location pair. Google's own ceiling is ~120. |
| Lead filters | |||
onlyWithContacts | Boolean | false | Save only businesses with an email or social profile. |
minRating | Select | none | none / 3 / 3.5 / 4 / 4.5. Unrated businesses drop when set. |
skipClosedPlaces | Boolean | true | Remove permanently / temporarily closed businesses. |
websiteFilter | Select | all | all / withWebsite / withoutWebsite. |
| Language & region | |||
language | Select | en | Google Maps interface language (hl). |
countryCode | Select | (any) | Bias results to a country (gl), and the countryCode column fallback. |
| Proxy | |||
proxyConfiguration | Object | { "useApifyProxy": true } | Apify Proxy settings. Apify Residential is rejected at startup. |
| Advanced | |||
extractEmails | Boolean | true | Scan website pages for email addresses. |
extractSocials | Boolean | true | Detect the six social platforms. |
maxCrawledPages | Integer | 3 | Pages fetched per website (homepage + contact / about). Max 5. |
deDuplication | Boolean | true | Skip businesses seen twice across overlapping searches. |
Provide at least one of queries or startUrls.
📈 Getting more results
Google Maps returns at most ~120 businesses per search, no matter how high maxResults goes. That is a limit of the Maps results feed, not of this Actor — every Maps scraper hits it.
The fix is Locations. One query across six areas is six searches, so roughly six times the ceiling:
{"queries": ["plumbers"],"locations": ["Brooklyn, NY", "Queens, NY", "Bronx, NY","Staten Island, NY", "Manhattan, NY", "Jersey City, NJ"],"maxResults": 700}
deDuplication is on by default, so overlapping areas won't produce duplicate rows, and searchQuery / searchLocation tell you which search each row came from.
Good search terms name a business type; the area belongs in Locations. Poor ones are single generic words, brand names you expect one result for, or country-wide sweeps.
🚦 Proxy policy
Use Apify Datacenter proxy (the default) or no proxy. Both work reliably for Google Maps and for shallow website fetches at this Actor's concurrency.
Apify Residential proxy is not supported — the run fails immediately, before it can cost you anything, if proxyConfiguration.apifyProxyGroups includes RESIDENTIAL. In a pay-per-event Actor, residential bandwidth is billed to the developer rather than to you, so a single bandwidth-heavy run can cost more than the leads it produces. Blocking it is exactly what lets this Actor charge one flat per-lead price with no bandwidth or platform-usage surcharge.
If you genuinely need residential routing, supply your own provider under the proxy editor's Custom proxy URLs — that traffic goes through your account, not Apify's, so the policy costs you nothing:
http://user:pass@proxy.iproyal.com:12321http://user:pass@proxy.brightdata.com:22225http://user:pass@proxy.oxylabs.io:7777
❓ FAQ
Why did I get fewer results than maxResults?
Google Maps caps a single search at roughly 120 businesses. Add more Locations — see Getting more results. If you have onlyWithContacts on, the run can also finish short: whether a website yields a contact is only known after it's been fetched.
Check searches_truncated in the run summary too. If it's above zero, Google stopped serving results early for at least one search. That's usually either an area with genuinely few businesses, or rate-limiting — running the same search repeatedly within a few minutes reliably triggers it. Wait a few minutes, or vary the query and locations.
Why is emails empty for some businesses?
Either the business has no website on its Maps listing, or its site shows no address on the pages we fetch. Many small businesses use a contact form instead of publishing an address — contactPageUrl gives you the form. (Addresses hidden by Cloudflare's email protection are decoded; that's a common case we handle.)
Some sites also sit behind bot protection that refuses any non-browser request. websites_blocked in the run summary counts those. It is a limitation of HTTP-only extraction and affects every scraper that does not run a full browser per site.
Do filters cost me anything?
No. A business removed by minRating, skipClosedPlaces, websiteFilter or onlyWithContacts is never saved and never charged, and it doesn't count toward maxResults. We absorb the discovery cost.
Do I need a proxy, and why is residential blocked? Datacenter proxy (the default) is enough. Residential is blocked because its bandwidth is developer-billed in a pay-per-event Actor — see Proxy policy for the workaround.
Can I import this into HubSpot, Google Sheets or my cold-email tool?
Yes — that's what the flat schema is for. Export the Contacts view as CSV and import it directly. No column is an array or a nested object, and primaryEmail gives mail-merge tools one address per row.
What is contactPageUrl for?
Plenty of businesses publish a contact form instead of an address, so the row has no email but is still reachable. contactPageUrl is the contact page the Actor actually opened and read during enrichment — never an unverified guess, so it won't hand your team a dead link. It's empty when the site has no recognisable contact page, or when maxCrawledPages is 1 (the homepage alone).
What's the difference between phone and websitePhones?
phone is the number on the Google Maps listing. websitePhones is anything published as a tel: link on the business's own site, with the Maps number removed — so it only ever tells you something new.
Is this legal? What about GDPR? The Actor reads only publicly available Google Maps listings and public business websites; it never logs in and never touches private data. Business contact details are generally public information, but you are responsible for how you use them — in the EU/UK that means a lawful basis for B2B outreach and an opt-out in every message. Check your local rules before sending.
Can I run it on a schedule or via the API? Yes — see Integrations & API.
What does is_enriched mean?
true when a row carries at least one of website, emails, or a social profile. It marks exactly the rows you're charged for, so charged_events in the run summary always matches the count of is_enriched: true rows.
My run finished with no results. Was it broken?
Check the run's status message — it says which it was: Google blocked the requests, your filters removed everything, or the search genuinely found nothing. RUN_SUMMARY breaks the same thing down numerically.
🔌 Integrations & API
- API — start runs and fetch the dataset with the Apify API or the official Python and JavaScript clients.
- Schedules — run daily or weekly from the Apify console; each run writes its own dataset.
- Webhooks — fire a
RUN.SUCCEEDEDwebhook at your own endpoint, Make or Zapier to push new leads into a CRM automatically. - No-code — Make, Zapier and Google Sheets integrations are available from the Actor's Integrations tab.
// JavaScript clientimport { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: 'YOUR_TOKEN' });const run = await client.actor('coregent/google-maps-email-social-enricher').call({queries: ['plumbers'],locations: ['Brooklyn, NY', 'Queens, NY'],maxResults: 200,});const { items } = await client.dataset(run.defaultDatasetId).listItems();
# Python clientfrom apify_client import ApifyClientclient = ApifyClient("YOUR_TOKEN")run = client.actor("coregent/google-maps-email-social-enricher").call(run_input={"queries": ["plumbers"],"locations": ["Brooklyn, NY", "Queens, NY"],"maxResults": 200,})items = client.dataset(run["defaultDatasetId"]).list_items().items
🚧 Limitations
- Search rows carry a street-level address. Google's results panel shows an address fragment rather than a full address, so
city,zipCodeandcountryCodeare usually blank on rows from a keyword search — a fragment has no city or country in it to read. They're filled on rows from a place URL and on listings with no website, where the Actor opens the full detail panel.latitudeandlongitudeare always populated, andsearchLocationrecords the area you searched regardless. Set the Country input to fillcountryCodeon every row. - Some websites refuse any non-browser request. Bot protection on a share of business sites returns 403 to an HTTP fetcher — including with a full desktop browser User-Agent, so it is not something a setting can work around. Those rows come back with no emails or socials, and
websites_blockedin the run summary counts them. This affects every scraper that reads sites over HTTP rather than running a real browser per site; we trade that coverage for speed and a low per-lead price. - ~120 businesses per search. A Google Maps limit, not an Actor one. Use Locations.
- Shallow website extraction. Homepage plus up to
maxCrawledPages - 1contact / about pages (3 total by default). No deep crawling, and no JavaScript rendering — sites that build their contact page entirely in the browser may return nothing. - Social profiles come from the business's own website, not from the platforms. If a business never links its Instagram, we won't find it.
- Website phone numbers come from
tel:links only. A free-text phone regex would capture dates, prices and company numbers, so numbers published only as plain text are not collected. reviewsCountis best-effort. Google does not always render a review count, and the same listing can return one on a run and not on the next. It is read from four independent places before giving up, so when the number exists it is captured; when it is absent the cell is empty rather than guessed.ratingis unaffected.contactPageUrlis only set for a page we actually fetched. WithmaxCrawledPagesset to 1 no contact page is opened, so the column stays empty.- No email verification, deliverability check or AI scoring. Addresses are returned as published.
- No login, cookie or session-based scraping.
city/zipCode/countryCodeparsing is best-effort across international address formats. When the Actor cannot identify a city with confidence it returns an empty cell rather than a guess, so the column is under-filled rather than wrong;addressis always the source of truth.- Hard caps: 5,000 results per run, 120 per search, 5 pages per website.
📊 Run summary
Every run writes a RUN_SUMMARY record to the key-value store:
{"inputs_total": 6,"businesses_found": 420,"websites_enriched": 301,"emails_found": 355,"charged_events": 338,"searches_blocked": 0,"searches_zero_results": 1,"searches_truncated": 1,"filtered_out": 62,"websites_blocked": 41,"websites_timeout": 7,"websites_failed": 12,"runtime_seconds": 540,"scraped_at": "2026-09-21T00:00:00.000Z"}
charged_events always equals the number of is_enriched: true rows saved, bounded by your per-run spending limit — an audit trail for every event you were billed for. searches_blocked, searches_zero_results, searches_truncated and filtered_out explain any shortfall without reading the log. The three websites_* counters account for every business whose website could not be read: websites_blocked is bot protection refusing our fetcher (401/403/429), websites_timeout is a site too slow to answer, websites_failed is everything else.
⭐ Feedback & support
Found a bug, or a field you need that isn't here? Open an issue on the Actor's Issues tab — we reply within 24 hours.
If the Actor saved you time, a review genuinely helps other people find it. Ratings are the main way Apify surfaces smaller Actors.
🛠️ Technical notes
- Stack: Node.js 18+ · Apify SDK 3 · Crawlee · Puppeteer (Maps) · Cheerio + native
fetch(websites). - Smart-hybrid discovery: a results card that already exposes a usable website is enriched over HTTP with no detail-panel navigation; only listings without one open the full panel. Fewer browser pages means a faster, cheaper run.
- Filters run before the cost. Rating, closed-business and website filters are decided from the results card, so a rejected listing never costs a browser navigation or a website fetch.
- Reliability: Maps concurrency 1–5 with a session pool, 3 retries per request, resource blocking (images, fonts, map tiles, ads) and consent-page handling. Website enrichment runs in a bounded 10-slot pool with a 15 s per-page timeout and a 45 s per-business budget.
- Memory: 1 GB minimum · 2 GB default · 4 GB maximum.
- Diagnostics: block and zero-result counts land in
RUN_SUMMARYand in the run's status message; on the first Maps render failure the page HTML is saved to the key-value store so a zero-result run can be diagnosed rather than guessed at.