Google Maps Email & Social Enricher avatar

Google Maps Email & Social Enricher

Pricing

from $2.40 / 1,000 lead-results

Go to Apify Store
Google Maps Email & Social Enricher

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

Delowar Munna

Maintained by Community

Actor stats

0

Bookmarked

14

Total users

1

Monthly active users

2 days ago

Last modified

Share

Google Maps Email & Social Media Extractor

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-result event 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 "", never null.
  • 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

  1. Enter one or more search queries ("dentists", "plumbers") and one or more locations ("Manchester, UK"). Every query is run in every location.
  2. Set Max results, and any Lead filters you want.
  3. 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 "".

Business leads — table view

Fields (30)

FieldTypeSourceNotes
placeIdstringGoogle MapsStable Google identifier; the de-duplication key.
titlestringGoogle MapsBusiness name.
categorystringGoogle MapsPrimary category, e.g. "Marketing agency".
ratingnumberGoogle MapsStar rating, "" when the business has none.
reviewsCountnumberGoogle MapsNumber of Google reviews. Best-effort: Google omits it on some listings.
permanentlyClosedbooleanGoogle Mapstrue when Google marks the business permanently or temporarily closed.
phonestringGoogle MapsAs Google displays it.
websitestringGoogle MapsTracking parameters (utm_*, gclid, fbclid, …) stripped.
primaryEmailstringWebsiteThe single best address: own domain first, then a named person over a department.
emailsstringWebsiteComma-separated. Junk and asset-filename addresses filtered out.
emailCountnumberWebsiteHow many addresses are in emails.
websitePhonesstringWebsiteNumbers published as tel: links, minus the one Maps already gave you.
contactPageUrlstringWebsiteThe business's contact page — where to go when they publish a form, not an email.
linkedinstringWebsiteOne canonical profile URL per platform; share / intent links dropped.
facebookstringWebsite
instagramstringWebsite
twitterstringWebsiteTwitter / X.
youtubestringWebsiteThe channel, not a video link.
tiktokstringWebsiteThe profile, not a single clip.
addressstringGoogle MapsSee Limitations — search rows carry a street-level fragment.
citystringGoogle MapsUsually blank on search rows — see the note under the samples.
zipCodestringGoogle MapsSame: present on place-URL rows, blank on most search rows.
countryCodestringGoogle MapsTwo-letter code. Needs a full address or the Country input.
latitudenumberGoogle MapsFrom the listing's Maps URL.
longitudenumberGoogle Maps
mapUrlstringGoogle MapsDirect link to the listing.
searchQuerystringYour inputThe query (or start URL) that produced this row.
searchLocationstringYour inputThe location it was crossed with — blank when you didn't use Locations.
scraped_atstringRunISO-8601 timestamp.
is_enrichedbooleanRuntrue 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 address is a street fragment, so city, zipCode and countryCode are 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 — searchLocation captures 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:

FieldSearch rowsPlace-URL rows
title, category, rating, website, mapUrl, coordinates100%100%
phone94%100%
reviewsCount100%varies — Google does not always publish a count
primaryEmail / emails~52%depends entirely on the site
contactPageUrl~58%—
linkedin / instagram / facebook42–48%—
twitter / youtube / tiktok10–36%—
websitePhones~12%—
city / zipCode / countryCode0–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:

EventCharged when
lead-resultOnce 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 wantListings processedFree planGold 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

FieldTypeDefaultPurpose
queriesArray[]What to search for, one per line.
locationsArray[]Areas to search in. Every query runs in every location.
startUrlsArray[]Google Maps place URLs and/or search URLs.
Result limits
maxResultsInteger50Run-wide cap on businesses saved (hard ceiling 5000).
maxResultsPerSearchInteger120Cap for each query × location pair. Google's own ceiling is ~120.
Lead filters
onlyWithContactsBooleanfalseSave only businesses with an email or social profile.
minRatingSelectnonenone / 3 / 3.5 / 4 / 4.5. Unrated businesses drop when set.
skipClosedPlacesBooleantrueRemove permanently / temporarily closed businesses.
websiteFilterSelectallall / withWebsite / withoutWebsite.
Language & region
languageSelectenGoogle Maps interface language (hl).
countryCodeSelect(any)Bias results to a country (gl), and the countryCode column fallback.
Proxy
proxyConfigurationObject{ "useApifyProxy": true }Apify Proxy settings. Apify Residential is rejected at startup.
Advanced
extractEmailsBooleantrueScan website pages for email addresses.
extractSocialsBooleantrueDetect the six social platforms.
maxCrawledPagesInteger3Pages fetched per website (homepage + contact / about). Max 5.
deDuplicationBooleantrueSkip 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:12321
http://user:pass@proxy.brightdata.com:22225
http://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.SUCCEEDED webhook 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 client
import { 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 client
from apify_client import ApifyClient
client = 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, zipCode and countryCode are 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. latitude and longitude are always populated, and searchLocation records the area you searched regardless. Set the Country input to fill countryCode on 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_blocked in 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 - 1 contact / 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.
  • reviewsCount is 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. rating is unaffected.
  • contactPageUrl is only set for a page we actually fetched. With maxCrawledPages set 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 / countryCode parsing 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; address is 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_SUMMARY and 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.