Google Maps Website & Contact Extractor avatar

Google Maps Website & Contact Extractor

Pricing

from $3.60 / 1,000 business-results

Go to Apify Store
Google Maps Website & Contact Extractor

Google Maps Website & Contact Extractor

Extract Google Maps business listings and enrich them with lightweight website contact details such as emails, contact page URL, phone numbers, and social profile links.

Pricing

from $3.60 / 1,000 business-results

Rating

0.0

(0)

Developer

Delowar Munna

Delowar Munna

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

Share

Google Maps Website & Contact Extractor

Extract Google Maps business listings by keyword + location and enrich each row with shallow website contact data — emails, contact page URL, website phone, and social profile links. Returns a clean, flat, CSV-friendly row per business — built for B2B lead generation, sales outreach, web design and SEO agencies, and local marketing.

V1 stays deliberately shallow on the website side (homepage + a small number of likely contact/about pages, capped by maxPagesPerWebsite) so runs are fast and cost-predictable. You pay one flat event per unique business row that passes your filters — same price whether or not the website extraction found extra contact data.

✨ Why this scraper

  • Maps + website in one pass — 41 flat fields covering Maps card data plus emails, contact page, website phone, and social links.
  • Shallow by design — homepage + up to maxPagesPerWebsite-1 extra contact/about pages per site. No deep crawling, no AI, no review scraping.
  • Pay-Per-Event — one flat event per saved business row, whether or not contact data was found. Duplicates and filtered rows are not charged.
  • No login, no cookies, no sessions — just keyword + location.
  • CSV-friendly output — flat structure, no nested objects, drops cleanly into Sheets/Excel/CRMs.
  • Transparent contact-quality score — rule-based (no AI), explained below.

🚀 Quick start — sample inputs

Example 1 — single query

{
"searchQueries": [{ "key": "plumbers", "value": "Canberra ACT" }],
"maxResults": 100,
"country": "AU",
"language": "en",
"websiteFilter": "hasWebsite",
"phoneRequired": false,
"emailRequired": false,
"includeWebsiteContactExtraction": true,
"maxPagesPerWebsite": 3,
"includeSocialLinks": true,
"includeWebsitePhone": true,
"includeOpeningHours": false,
"includeCoordinates": true,
"deduplicateResults": true,
"proxyConfiguration": { "useApifyProxy": true }
}

Example 2 — multi-query, email required, custom residential proxy via your own provider

{
"searchQueries": [
{ "key": "electricians", "value": "Sydney NSW" },
{ "key": "dentists", "value": "Melbourne VIC" },
{ "key": "cafes", "value": "Brisbane QLD" }
],
"maxResults": 200,
"country": "AU",
"language": "en",
"websiteFilter": "hasWebsite",
"phoneRequired": true,
"emailRequired": true,
"includeWebsiteContactExtraction": true,
"maxPagesPerWebsite": 3,
"includeSocialLinks": true,
"includeWebsitePhone": true,
"includeOpeningHours": false,
"includeCoordinates": true,
"deduplicateResults": true,
"proxyConfiguration": {
"useApifyProxy": false,
"proxyUrls": ["http://user:pass@proxy.iproyal.com:12321"]
}
}

The actor blocks Apify Residential proxy; if you need residential routing, supply your own provider via proxyConfiguration.proxyUrls as shown. See 🚦 Proxy policy below.

The searchQueries field uses Apify's Key/Value form editor — the Key column is the business keyword (e.g. plumbers), the Value column is the location (e.g. Canberra ACT). Add one row per search.


📦 Output

The dataset has one view: Business contact leads — a 41-column flat table.

Business contact leads — table view

Output fields (41)

search_keyword, search_location, business_name, category, rating, review_count, maps_phone, website_phone, primary_phone, website, website_domain, address, city, state, postcode, country, google_maps_url, place_id, latitude, longitude, opening_hours, has_website, has_maps_phone, has_website_phone, emails, primary_email, email_count, email_types, contact_page_url, facebook_url, instagram_url, linkedin_url, x_url, youtube_url, social_links_count, website_pages_scanned, website_status, contact_quality_score, contact_quality_label, contact_tags, scraped_at.

Sample record — Business contact leads

{
"search_keyword": "electricians",
"search_location": "New York NY",
"business_name": "PK & Altman Electric",
"category": "Electrical installation service",
"rating": 4.5,
"review_count": null,
"maps_phone": "(646) 630-7164",
"website_phone": "212-673-9400",
"primary_phone": "(646) 630-7164",
"website": "https://pkelectricnyc.com/?utm_campaign=gmb",
"website_domain": "pkelectricnyc.com",
"address": "94 E 4th St",
"city": "",
"state": "",
"postcode": "",
"country": "",
"google_maps_url": "https://www.google.com/maps/place/PK+%26+Altman+Electric/",
"place_id": "0x4065fc94aff64f07:0xa7c23f01be65d9f9",
"latitude": 40.7257673,
"longitude": -73.9889033,
"opening_hours": "",
"has_website": true,
"has_maps_phone": true,
"has_website_phone": true,
"emails": "info@pkelectricnyc.com, office@pkelectricnyc.com",
"primary_email": "office@pkelectricnyc.com",
"email_count": 2,
"email_types": "info,generic",
"contact_page_url": "https://pkelectricnyc.com/contact/",
"facebook_url": "https://facebook.com/PKAltmanElectric",
"instagram_url": "",
"linkedin_url": "",
"x_url": "",
"youtube_url": "https://youtube.com/@pkaltmanelectric",
"social_links_count": 2,
"website_pages_scanned": 2,
"website_status": "success",
"contact_quality_score": 100,
"contact_quality_label": "Excellent Contact Lead",
"contact_tags": [
"has_website",
"has_maps_phone",
"has_website_phone",
"has_email",
"has_primary_email",
"has_contact_page",
"has_social_links",
"contact_ready"
],
"scraped_at": "2026-05-08T11:33:06.777Z"
}

🎯 Contact-quality score

Transparent rule-based score (0–100) computed from extracted fields — no AI, no external enrichment.

SignalPoints
Has website+15
Has Maps phone+15
Has website phone+10
Has at least one email+30
Has primary email+10
Has contact page URL+10
Has at least one social+10
Has address+5
Has category+5

Score is capped at 100.

Labels: Excellent Contact Lead (80–100) · Good Contact Lead (60–79) · Basic Contact Lead (40–59) · Low Contact Data (0–39).

contact_tags includes contact_ready whenever primary_email or primary_phone is present — sort by this tag for a clean call/email list.


💰 Pricing

Pay-Per-Event. One flat event per saved row (final per-event price is configured on the Apify console):

EventCharged when
business-resultOnce per unique business row that passed all filters and was successfully written to the dataset — whether or not the website extraction found additional contact data.

So your bill is simply results_saved × price_per_event. Rows where website enrichment found emails / contact pages / socials cost the same as Maps-only rows; the price is averaged across both kinds.

The actor honors the user-configured per-run spending cap (Apify eventChargeLimitReached) and stops cleanly when reached.

🚦 Proxy policy

Use Apify Datacenter proxy or no proxy for normal runs — both work reliably for Google Maps search and shallow website fetches at this actor's conservative concurrency.

Apify Residential proxy is not supported. The actor will fail at startup if proxyConfiguration.apifyProxyGroups includes RESIDENTIAL. Reason: in pay-per-event actors, residential bandwidth (~/GB) is billed to the developer, not the run user, so a single bandwidth-heavy run could exceed the per-result event revenue.

If you genuinely need residential routing, supply your own residential provider via the proxy editor's Custom proxy URLs field — that traffic goes through your provider, not Apify, and is unaffected:

http://user:pass@proxy.iproyal.com:12321
http://user:pass@proxy.brightdata.com:22225
http://user:pass@proxy.oxylabs.io:7777

Not charged:

  • Duplicates (de-duplicated by place_id, listing URL, website domain, or name+address/phone).
  • Rows filtered out by websiteFilter, phoneRequired, or emailRequired.
  • Rows missing a business_name.
  • Failed dataset pushes.
  • Failed website fetches when no row was saved.
  • Anything after the per-run spending cap is reached.

📊 Run summary

After each run, a RUN_SUMMARY entry is written to the key-value store:

{
"inputs_total": 2,
"successful_inputs": 2,
"failed_inputs": 0,
"raw_results_found": 180,
"results_saved": 100,
"business_events_charged": 100,
"duplicates_removed": 18,
"filtered_out": 62,
"websites_attempted": 82,
"websites_succeeded": 68,
"websites_failed": 10,
"websites_timed_out": 4,
"emails_found": 75,
"blocked_requests": 0,
"retry_count": 6,
"runtime_seconds": 240,
"scraped_at": "2026-05-07T00:00:00.000Z"
}

business_events_charged always equals results_saved.


⚙️ Filters

FilterStageEffect
websiteFilterPre-extractionany / hasWebsite / missingWebsite. Default hasWebsite.
phoneRequiredPost-extractionIf true, keep rows where maps_phone or website_phone is present.
emailRequiredPost-extractionIf true, keep only rows where at least one website email was found.
deduplicateResultsBoth stagesDrop duplicates across queries (recommended ON).

Filters are applied before dataset push or event charges.


🚧 Limitations (V1)

  • Cards-only Maps extraction: V1 reads each business directly from the search results panel and does not click into individual place detail panels. Phone, website, full opening hours, and place_id only appear when Google surfaces them on the card itself; otherwise these fields are empty/null.
  • Shallow website extraction: homepage + up to maxPagesPerWebsite-1 extra pages (default 3 total). No deep crawling, no JS-heavy rendering by default.
  • No email verification, deliverability checks, or AI scoring.
  • No full review text, sentiment, photos, menus, prices, or popular times.
  • No login/cookie/session-based scraping.
  • Address parsing into city/state/postcode is best-effort; the full address field is the source of truth.
  • Per-query hard cap is 500 results; per-run hard cap is 5,000 results; per-website page cap is 5.

❓ FAQ

Why is email_count zero on some businesses?
Many local businesses don't publish an email publicly — they use a contact form instead. The actor extracts only emails visible in the page HTML (visible text + mailto: links). Set emailRequired: true if you want to keep only rows with at least one extracted email.

Why are city, state, postcode empty on some rows?
The Google Maps card sometimes surfaces only a single-line street address. The full address field is the source of truth; city/state/postcode populate only when Maps shows a comma-separated multi-segment address. For full address normalization, post-process with a geocoder of your choice.

Can I use Apify Residential proxy?
No — the actor rejects Apify Residential at startup. Apify Datacenter, no proxy, and user-supplied custom proxy URLs all work fine. If you genuinely need residential routing for a specific region or site, supply your own provider (IPRoyal, BrightData, Oxylabs, etc.) via the proxy editor's Custom proxy URLs field — that traffic bypasses Apify billing entirely. See the 🚦 Proxy policy section above.

Can I export to CSV?
Yes — every field is flat (no nested objects). Use Apify's CSV / Excel export from the dataset page, or call the dataset API with format=csv.

How am I billed for rows that don't have any extracted emails or socials?
The same as rows that do — one flat business-result event per saved row. The per-event price is averaged across both outcomes, so a run with high enrichment hit-rate and a run with low hit-rate cost the same per row. If you only need Maps directory data (no website enrichment at all), set includeWebsiteContactExtraction: false to skip the website fetches — billing is unchanged but the run is faster.

How do I get more results per query?
The per-query hard cap is 500 (per-run cap 5,000). For more, split your search into narrower geographies or sub-niches and run them as separate queries — the actor dedupes across queries within a run.

Will I get blocked?
The actor uses conservative concurrency (Maps min=1, max=3; website pool =5), HTTP-only website fetching, realistic headers, and respects retry/backoff. Default Apify Proxy is sufficient for typical lead-gen volumes. If a specific site or region blocks you, switch the proxy selector to Residential for that run.

What does website_status mean?

  • not_attempted — website extraction was disabled or there was no website to fetch.
  • no_website — Maps had no website link for that business.
  • success — homepage fetched successfully (regardless of whether emails were found).
  • failed — the homepage returned an HTTP error or the network call failed.
  • timeout — the request was cut off by the per-page (15s) or per-business (45s) timeout.

🛠️ Technical notes

  • Stack: Node.js 18+ · Apify SDK 3 · Crawlee · Puppeteer (Maps) · Cheerio + native fetch (websites).
  • Concurrency: Maps min=1, max=3 (conservative); website pool concurrency=5.
  • Memory: 1 GB min · 2 GB default · 4 GB max.
  • Proxy: Apify Proxy enabled by default; custom configs accepted.
  • Diagnostics: On the first failed Maps render (no feed, or feed but zero cards), the actor saves the page HTML and URL to the key-value store as debug-no-feed-html / debug-zero-cards-html.