Google Maps Email Enrichment - Local Business Leads
Pricing
$5.00 / 1,000 lead with contacts
Google Maps Email Enrichment - Local Business Leads
Google Maps enricher for business leads enrichment: feed it Google Maps Scraper results and get local business emails, phones, socials, team-page people and tech stack. Found an email for 81% of 80 real test businesses. $0.005 per lead with contacts; the rest is free.
Pricing
$5.00 / 1,000 lead with contacts
Rating
0.0
(0)
Developer
Luke Hunter
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Share
A Google Maps enricher for agencies, sales teams, freelancers and growth teams who scrape Google Maps and then need local business emails. This is business leads enrichment: Google Maps email enrichment, Google Maps contacts and the Google Maps tech stack of each business, from the business's own website. Run a Google Maps scraper, pick its results here, and get one enriched row per business: the emails the business publishes on its own website (each with the page it came from, the best one marked as primaryEmail), its phone numbers, social links, the named people with job titles from its team page, and the technology stack of its site (CMS, ecommerce platform, analytics, ad pixels, email marketing, payments, chat), plus simple yes/no lead flags (hasEmail, isEcommerce, runsAds, usesEmailMarketing, ...). Your own Google Maps columns (name, category, address, rating, place ID, ...) come back next to the result under source, so everything joins to the rows you started with.
It enriches any existing results, so you keep using the Google Maps scraper you already like, and you pay per lead found: $0.005 per business with an email or phone - 1,000 leads = $5. Businesses with no website, nothing found, a site that blocked the request, or that your filters removed are never charged.
Google Maps email enrichment: quick start (3 steps)
- Run a Google Maps scraper (for example Google Maps Scraper by Compass) and wait until the run has finished.
- Start this Actor and pick that run's dataset in Your Google Maps Scraper results (dataset). By hand: open the scraper run's Storage tab, copy the dataset ID (the code at the end of
.../storage/datasets/WkzbQMuFYuamGv3YF), and paste it. Raise Maximum leads to the number of businesses you want (default 100, up to 10,000). - Export the dataset as CSV, Excel or JSON from the run's Storage tab, or pull it through the API (see below).
{"datasetId": "WkzbQMuFYuamGv3YF","maxItems": 1000}
No Google Maps run yet? The form is prefilled with three example websites (plausible.io, mailerlite.com, stumptowncoffee.com) so you can see what a row looks like for about $0.015. They are ignored as soon as you choose a dataset. You can also paste a list of websites or give a link to a CSV file instead of a dataset (see "Other ways to give it businesses").
Chain it automatically (Integrations)
Every Google Maps run can be enriched the moment it ends, with nothing to click:
- In Apify Console open your Google Maps scraper (or its saved Task) and go to Integrations.
- Add an Actor integration, choose Google Maps Email Enrichment (
lukehunter/lead-enricher), and set the trigger to Run succeeded. - Use this as the integration's input:
{"datasetId": "{{resource.defaultDatasetId}}","maxItems": 1000}
{{resource.defaultDatasetId}} is filled in by Apify with the dataset of the run that just finished. Add the other inputs as usual (requireTech, onlyWithEmail, ...). Prefer a webhook? Use the event ACTOR.RUN.SUCCEEDED on the scraper, call this Actor's run endpoint, and put the same datasetId in the payload template. The dataset is read with your own account's access, so you never hand over a token.
Tested on real businesses
We ran it on 80 real business websites (dentists, agencies, shops, gyms, venues and more, each checked first for robots.txt permission) on 2026-10-10:
| Result | Share of businesses |
|---|---|
| At least one email found | 81% |
| Phone number found | 71% |
| Named person (team page) found | 34% |
| Tech stack detected | 88% |
| Site blocked us or unreachable (free) | 9% |
| Nothing published (free) | 6% |
You pay only for the businesses where an email or phone was found. Results vary by industry and country; small sites with only a contact form have no email to find. Aggregate numbers only: no contact data from the test is published or stored.
Example output: Google Maps contacts and local business emails
One row per business (a chain with several Google Maps listings and one website is one business, charged once). An example, produced by this Actor from the real stumptowncoffee.com home page (captured 2026-10-04) and a Google Maps Scraper row (the Maps row is an invented sample; a live run reads up to six pages, so it may list more, and emailDomainHasMx and checkedAt are filled in at run time):
{"companyName": "Stumptown Coffee","domain": "stumptowncoffee.com","primaryEmail": "info@stumptowncoffee.com","emails": [{ "email": "info@stumptowncoffee.com", "kind": "generic", "person": null, "ownDomain": true, "sourceUrl": "https://www.stumptowncoffee.com/" }],"phones": [{ "phone": "877-711-3385", "e164": null, "sourceUrl": "https://www.stumptowncoffee.com/" }],"socials": {"linkedin": null,"facebook": "https://www.facebook.com/stumptowncoffee","instagram": "https://www.instagram.com/stumptowncoffee","x": "https://twitter.com/stumptowncoffee","youtube": "https://www.youtube.com/channel/UC0yk_H5Np-uGvy98UM-jj6w","tiktok": "https://www.tiktok.com/@stumptowncoffee"},"people": [{ "name": "Duane Sorenson", "title": "Founder", "email": null, "linkedin": null, "sourceUrl": "https://www.stumptowncoffee.com/" }],"techStack": {"cms": null,"ecommerce": "Shopify","analytics": [],"marketing": ["Klaviyo"],"payments": ["Shop Pay"],"chat": [],"technologies": [{ "name": "Shopify", "category": "Ecommerce", "confidence": "high", "evidence": "meta shopify-digital-wallet tag present" },{ "name": "Klaviyo", "category": "Marketing", "confidence": "high", "evidence": "inline script contains _learnq" },{ "name": "Shop Pay", "category": "Payments", "confidence": "high", "evidence": "script src contains shopifycloud/shop-js" },{ "name": "Shopify CDN", "category": "CDN", "confidence": "high", "evidence": "script src contains cdn.shopify.com" }]},"leadFlags": {"hasEmail": true,"hasPhone": true,"hasNamedContact": true,"isEcommerce": true,"runsAds": false,"usesEmailMarketing": true},"contactPageUrl": null,"address": { "street": "700 SW 5th Ave, 3rd Floor, #400", "locality": "Portland", "region": "OR", "postalCode": "97214", "country": null },"emailDomainHasMx": true,"status": "ok","statusReason": null,"inputUrl": "https://www.stumptowncoffee.com/","finalDomain": "stumptowncoffee.com","pagesCrawled": 1,"changeType": null,"changedFields": null,"checkedAt": "2026-10-04T12:00:00.000Z","source": {"title": "Stumptown Coffee Roasters","categoryName": "Coffee roasters","address": "128 SW 3rd Ave, Portland, OR 97204, United States","city": "Portland","phone": "(503) 295-6144","totalScore": 4.5,"reviewsCount": 1342,"url": "https://www.google.com/maps/search/?api=1&query=Stumptown%20Coffee%20Roasters&query_place_id=ChIJSyntheticStumptown0001","placeId": "ChIJSyntheticStumptown0001","datasetRow": 0,"sourceRowCount": 2,"otherRows": ["ChIJSyntheticStumptown0002"]}}
e164 is null here because a US-looking number on a .com is not enough to be sure of the country; the Actor never guesses.
A business whose team page names its people gives a named contact. This row is synthetic (an invented dental practice used in the Actor's tests), showing primaryEmail choosing the named dentist over the reception mailbox, and the flags:
{"domain": "brightsmile-dental.co.uk","primaryEmail": "nadia.rahman@brightsmile-dental.co.uk","emails": [{ "email": "nadia.rahman@brightsmile-dental.co.uk", "kind": "person", "person": "Nadia Rahman", "ownDomain": true, "sourceUrl": "http://www.brightsmile-dental.co.uk/meet-the-team/" },{ "email": "reception@brightsmile-dental.co.uk", "kind": "generic", "person": null, "ownDomain": true, "sourceUrl": "http://www.brightsmile-dental.co.uk/" }],"people": [{ "name": "Nadia Rahman", "title": "Principal Dentist", "email": "nadia.rahman@brightsmile-dental.co.uk", "linkedin": null, "sourceUrl": "http://www.brightsmile-dental.co.uk/meet-the-team/" },{ "name": "Owen Whitfield", "title": "Associate Dentist", "email": null, "linkedin": null, "sourceUrl": "http://www.brightsmile-dental.co.uk/meet-the-team/" },{ "name": "Karen Doyle", "title": "Practice Manager", "email": null, "linkedin": null, "sourceUrl": "http://www.brightsmile-dental.co.uk/meet-the-team/" }],"leadFlags": { "hasEmail": true, "hasPhone": true, "hasNamedContact": true, "isEcommerce": false, "runsAds": true, "usesEmailMarketing": true },"status": "ok"}
A row of your dataset that has no website comes back free, with the same columns, empty, and your source fields, so you can see which leads could not be enriched (and phone them instead):
{"companyName": "Corner Cottage Dental","domain": null,"primaryEmail": null,"emails": [],"phones": [],"people": [],"leadFlags": { "hasEmail": false, "hasPhone": false, "hasNamedContact": false, "isEcommerce": false, "runsAds": false, "usesEmailMarketing": false },"status": "no-website","statusReason": "This row of your dataset has no website to read: the only value found, in \"url\", is a Google Maps or Google search link (www.google.com). Nothing was fetched and nothing is charged.","source": { "title": "Corner Cottage Dental", "categoryName": "Dentist", "city": "Leeds", "phone": "0113 496 0188", "placeId": "ChIJSyntheticCorner0009", "datasetRow": 8, "sourceRowCount": 1, "otherRows": [] }}
(Rows also carry techStack, socials, address and the other columns, with empty or null values; they are left out of this example for space.)
The columns
| Category | Fields |
|---|---|
| Identity | companyName, domain, inputUrl, finalDomain |
| Best contact | primaryEmail |
| Contacts | emails (email, kind, person, ownDomain, sourceUrl), phones (phone, e164, sourceUrl), socials (linkedin, facebook, instagram, x, youtube, tiktok) |
| People | people (name, title, email, linkedin, sourceUrl; at most 50 per business) |
| Tech stack | techStack.cms, techStack.ecommerce, techStack.analytics[], techStack.marketing[], techStack.payments[], techStack.chat[], and the full techStack.technologies[] (name, category, confidence, evidence) |
| Qualification | leadFlags: hasEmail, hasPhone, hasNamedContact, isEcommerce, runsAds, usesEmailMarketing |
| Company | contactPageUrl, address (JSON-LD PostalAddress only) |
| Quality | emailDomainHasMx (public DNS MX lookup of primaryEmail's domain), pagesCrawled |
| Source row | source (only when a dataset was used: the Google Maps columns below plus yours; null for a website you pasted) |
| Status | status (ok, no-contacts-found, blocked, unreachable, robots-disallowed, no-website), statusReason, changeType and changedFields (change mode), checkedAt |
primaryEmailis the best address to write to: an address on the business's own domain beats one on another domain, then a named person beats a role mailbox (sales@,support@,bookings@) beats a generic one (info@,hello@,contact@), then the order the site gave them in.kindsays which it is:person(the site ties the address to a named person, or it is writtenfirst.last@or is a lone common first name such assarah@),roleorgeneric. It is judged only from the published address and the people the site lists: nothing is ever built or guessed.leadFlags:hasNamedContactis true when the site publishes a named person or an email that belongs to one.isEcommerce: an ecommerce platform was detected.runsAds: an advertising pixel or tag (Meta Pixel, Google Ads, TikTok Pixel, LinkedIn Insight Tag, ...) is on the home page.usesEmailMarketing: Klaviyo, Mailchimp, HubSpot, Marketo, ActiveCampaign, Omnisend, Brevo, ConvertKit or MailerLite was detected.sourcealways has the Google Maps columnstitle,categoryName,address,city,phone,totalScore,reviewsCount,url(the Google Maps link) andplaceId(whichever the row has), plus any extra columns you list in Dataset: extra columns to copy intosource(up to 15, text cut at 2,000 characters), plusdatasetRow(the row's number in the dataset, counting from 0),sourceRowCountandotherRows(for a chain: the other Maps listings that share the website, at most 20). Join onsource.placeId.
Business leads enrichment filters: pay only for the leads you want
Filters run before a row is delivered or charged, so a row they remove is not in your dataset, not charged, and does not count toward Maximum leads.
- Only leads with an email (
onlyWithEmail, on by default). Off, you also get businesses with a phone number only (charged, a phone is a lead) and, free, rows that show why a site gave nothing. - Only sites that run (
requireTech): at least one of these technologies, found on the home page. Skip sites that run (excludeTech): none of these. Names are matched ignoring case and punctuation; a name the Actor cannot detect is an input error with suggestions. All 166 detectable technologies are listed below.
Recipe: only Shopify stores with an email
{"datasetId": "WkzbQMuFYuamGv3YF","requireTech": ["Shopify"],"onlyWithEmail": true,"maxItems": 500}
Recipe: dentists with a named contact. Scrape "dentist in Leeds" with the Google Maps scraper, enrich it here, then keep the rows where leadFlags.hasNamedContact is true (in the dataset view, the CSV, or your CRM); primaryEmail is already the named person's address where the site publishes one, and people has the dentists' names and titles.
{"datasetId": "WkzbQMuFYuamGv3YF","includePeople": true,"maxPagesPerSite": 8,"maxItems": 500}
Recipe: agencies selling email marketing. Skip businesses that already run it, and keep ones that advertise: "excludeTech": ["Klaviyo", "Mailchimp", "HubSpot"], then filter leadFlags.runsAds in your sheet.
To bound a run when filters remove many rows, a run reads at most 10 x Maximum leads websites (at least 100); the status message says if that cap was reached.
Other ways to give it businesses
- Paste websites into Websites: domains, URLs with a path, email addresses (the company domain is used; free-mail such as gmail is skipped) or a whole spreadsheet column or CSV row. A URL with a path is read as the first extra page. De-duplicated by registrable domain.
- A link to a CSV or TXT file in Website list file URL (a Google Sheets "Publish to web" CSV link, Dropbox shared link, GitHub raw link): downloaded once (at most 5 MB, 20,000 lines), every cell read like a pasted entry.
- Both are added to the dataset's businesses and de-duplicated with them. Everything skipped (invalid entries, free-mail, duplicates, dataset rows with no usable website) is listed with its reason in the run status and in the
INPUT_REPORTrecord of the run's key-value store.
Which column holds the website? The one you name in Dataset: website column, else the first of website, websiteUrl, web, url, domain, homepage, companyWebsite, company_website, site that has a value (Google Maps Scraper uses website). A Google Maps, social network (Facebook, Instagram, LinkedIn, X, ...) or marketplace link is never read as a company website, whichever column it is in. A business whose only "website" is a Facebook page is a no-website row.
How it works
For each business the Actor:
- Reads
robots.txtof the host (once per host) and checks every path against it before requesting it. A disallowed path is never fetched. - Reads the home page, then the same-site pages most likely to carry contacts and people (contact, kontakt, contacto, contatti, impressum, mentions legales, about, team, leadership, support, locations and similar, best match first), up to Pages per website (default 6; if the home page links no contact or team page it reads one sitemap file to find them).
- Extracts what the pages literally publish:
mailto:links,tel:links, JSON-LD, addresses written in the text, simple encodings of an address that is already on the page (name [at] domain [dot] com,@, Cloudflare's obfuscation), clearly labelled phone numbers, social profile links and the people on team pages. - Detects the technology stack on the home page it already fetched - the scripts, links, meta tags, markup, response headers and cookie names, matched against 166 curated signatures. No extra request is made for the tech stack.
- Delivers one row per business, the best email first, with the page each contact came from.
It reads politely: about one request per second per site (slower when a site's robots.txt asks for a Crawl-delay), 5 sites at a time, an honest self-identifying User-Agent, a time budget per site, at most three redirects per page. If a site blocks it, the row says blocked and the Actor stops requesting that site instead of working around it.
Google Maps tech stack: the technologies it detects
CMS, ecommerce, analytics, ad pixels, email marketing, chat, payments and more: 166 in all.
- CMS: WordPress, Wix, Squarespace, Webflow, Drupal, Joomla, Ghost, HubSpot CMS, Framer, TYPO3
- Ecommerce: Shopify, WooCommerce, Magento, BigCommerce, PrestaShop, Shopware, Squarespace Commerce, Ecwid, Salesforce Commerce Cloud, Snipcart
- Analytics: Google Analytics 4, Universal Analytics, Google Tag Manager, Plausible, Fathom, Matomo, Hotjar, Microsoft Clarity, Mixpanel, Segment, Amplitude, Heap, Adobe Analytics, Adobe Experience Platform Launch, Simple Analytics, Umami, PostHog, FullStory, Optimizely, VWO, Cloudflare Web Analytics, Vercel Analytics, Yandex Metrica
- Advertising: Meta Pixel, TikTok Pixel, LinkedIn Insight Tag, Pinterest Tag, Google Ads, Google AdSense, Google Ad Manager, X (Twitter) Pixel, Snapchat Pixel, Reddit Pixel, Microsoft Advertising
- Marketing: Klaviyo, Mailchimp, HubSpot, Marketo, ActiveCampaign, Omnisend, Brevo, ConvertKit, Attentive, MailerLite
- Chat: Intercom, Drift, Zendesk, Crisp, Tawk.to, LiveChat, Tidio, Gorgias, Freshchat, Olark, Help Scout Beacon
- Payments: Stripe, PayPal, Klarna, Afterpay, Shop Pay, Square, Braintree, Adyen, Amazon Pay, Affirm, Mollie, Paddle, Razorpay
- Reviews: Trustpilot, Yotpo, Judge.me, Okendo, Reviews.io, Stamped.io, Loox
- CDN: Cloudflare, Fastly, Akamai, AWS CloudFront, Shopify CDN
- Hosting: Vercel, Netlify, WP Engine, Kinsta, GitHub Pages, Heroku, Pantheon, Google Cloud
- Security: Imperva, reCAPTCHA, hCaptcha, Cloudflare Turnstile
- JavaScript framework: React, Next.js, Vue.js, Nuxt, Angular, AngularJS, Svelte, SvelteKit, Gatsby, Remix, Astro, Alpine.js, htmx
- JavaScript library: jQuery, jQuery UI
- UI framework: Bootstrap, Tailwind CSS
- Web framework: ASP.NET, Express, Laravel, Ruby on Rails, Django
- Web server: Nginx, Apache, LiteSpeed, Microsoft IIS
- Programming language: PHP
- Consent: OneTrust, Cookiebot, CookieYes, Usercentrics, Termly, iubenda, Osano, Complianz
- Search: Algolia
- Fonts: Font Awesome, Google Fonts, Adobe Fonts
- Maps: Google Maps
- Video: YouTube, Vimeo
- Page builder: Elementor, Divi
- SEO: Yoast SEO, Rank Math
- Monitoring: Sentry, New Relic, LogRocket
- Scheduling and forms: Calendly
- JavaScript CDN: jsDelivr, cdnjs, unpkg, Google Hosted Libraries
Every match carries its confidence and the evidence that matched ("script src contains cdn.shopify.com", "meta generator WordPress 6.5.2", "cookie _shopify_y"), so you can check any claim.
Input
| Field | Type | Default | Description |
|---|---|---|---|
datasetId | string | (one of datasetId, websites or websitesFileUrl) | Your Google Maps Scraper (or any scraper) results: the dataset of a finished run. Use {{resource.defaultDatasetId}} in an integration. Read with your own account's access, no token needed; up to 20,000 rows. |
datasetWebsiteField | string | (automatic) | Column of the dataset that holds the website. Automatic: the first non-empty of website, websiteUrl, web, url, domain, homepage, companyWebsite, company_website, site. |
datasetPassthroughFields | string[] | [] | Extra columns to copy into source, on top of the always-copied Google Maps columns. At most 15. |
websites | string[] | (prefilled sample) | Domains, URLs or email addresses, one per line or a whole column. Up to 20,000. The prefilled sample is ignored when you choose a dataset. |
websitesFileUrl | string | (none) | https link to a .csv or .txt file of websites. |
onlyWithEmail | boolean | true | Deliver only businesses with an email found. Off: phone-only businesses (charged) and free explanatory rows too. |
requireTech | string[] | [] | Deliver only businesses running at least one of these technologies. |
excludeTech | string[] | [] | Drop businesses running any of these technologies. |
includeNoWebsiteRows | boolean | true | Return dataset rows with no usable website as free no-website rows. They never count toward maxItems. |
maxItems | integer | 100 | 1-10,000. Hard cap on business rows delivered in the run; also your cost cap (at most maxItems x $0.005). |
maxPagesPerSite | integer | 6 | 1-20. Pages read per website, home page included. |
includePeople | boolean | true | Read the people the site publishes into people. |
outputFormat | string | full | full: every field (default). compact: a smaller row for AI agents (see "Use with AI agents (MCP)"). Charging is identical. |
onlyChangedSinceLastRun | boolean | false | Change mode: deliver a business only when it is new or its contacts or tech stack changed. |
stateStoreName | string | lead-enricher-state | Name of the store that remembers each business's state in change mode. |
Optional Notifications (Discord, Telegram, Slack, Teams, Google Chat, ntfy, Pushover or a generic JSON webhook) are described below.
Input details
The Store form and the MCP tool schema show a short description for each field; the longer rules are here.
datasetId: the dataset of a finished run of Google Maps Scraper by Compass (compass/crawler-google-places) or any scraper whose rows carry a website. To chain automatically, open the scraper's Integrations tab, add this Actor and set the field to{{resource.defaultDatasetId}}; by hand, copy the dataset ID from the run's Storage tab. A link to the dataset in Console or the API is accepted too. The dataset is read with your own account's access, so you never give a token. Up to 20,000 rows are read. Rows whose website is a Google Maps, social network or marketplace link are never read as a company website. It is added towebsitesandwebsitesFileUrland de-duplicated with them.datasetWebsiteField: the column is matched exactly as spelled. Left empty, the first non-empty ofwebsite,websiteUrl,web,url,domain,homepage,companyWebsite,company_website,siteis used (Google Maps Scraper useswebsite). A value that is a Google Maps, social network (Facebook, Instagram, LinkedIn, X, YouTube, TikTok, ...) or marketplace link is never read as a website, whichever column it is in.datasetPassthroughFields: these Google Maps columns are always copied intosource(whichever the row has):title,categoryName,address,city,phone,totalScore,reviewsCount,url,placeId. List any further columns here (for exampleplusCode,state,postalCode,price), at most 15. Text is cut at 2,000 characters; a list or object is copied as JSON text.sourcealso always hasdatasetRow,sourceRowCountandotherRows(at most 20); those three names are reserved. When several rows have the same website, the first row's columns are kept.websites: paste anything, one per line or a whole spreadsheet column or CSV row. Each entry may be a domain (plausible.io), a URL with a path, query orwww(https://www.mailerlite.com/contact), an email address (its domain is used; free-mail addresses such as gmail, outlook or yahoo are skipped), or several of these on one line separated by commas, semicolons, spaces or tabs. The home page is always read first, then the contact, impressum, about, team, leadership, support and locations pages of the same site (up tomaxPagesPerSite). Entries are de-duplicated by registrable domain. Entries that cannot be used are never an error: they are skipped and listed, with the reason, in the run status and in theINPUT_REPORTrecord of the run's key-value store. The three prefilled sample websites are a first test run: they are ignored when you choose a dataset and leave them unchanged.websitesFileUrl: for example a Google Sheets "Publish to web" CSV link (File > Share > Publish to web > "Comma-separated values (.csv)"), a Dropbox shared link or a GitHub "Raw" link. The file is downloaded once (at most 5 MB and 20,000 lines) and every cell is read exactly like an entry ofwebsites. The link must open without signing in. The file is your own input list, sorobots.txtis not applied to downloading it; it is never requested from private or internal addresses. The businesses' websites are still read politely, withrobots.txtchecked for every page.onlyWithEmail: on by default. A business with no email (phone only, nothing found, blocked, unreachable) is dropped before delivery, so it is not in your dataset and not charged. Off: businesses with a phone number only (charged, a phone counts as a lead) and, free of charge, rows that show why a site gave nothing (no-contacts-found,blocked,unreachable,robots-disallowed).requireTech: applied before charging, matched ignoring case and punctuation, against the technologies found on the home page. A name this Actor cannot detect is an input error with suggestions. Businesses whose site could not be read have no technologies, so they are dropped when this is set.maxItems: rows removed by the filters are not delivered and do not count; rows of businesses with nothing found (only whenonlyWithEmailis off) are delivered free and count. To keep a run bounded when filters remove many rows, at most 10 xmaxItemswebsites (at least 100) are read per run.maxPagesPerSite: contact-like and team-like pages are read first, in many languages (contact, kontakt, contacto, contatti, impressum, mentions legales, about, team, leadership, equipo, equipe, ueber uns, support, locations, ...). Each site is read at about one page per second, 5 sites at a time. The technology stack is detected on the home page, which is always read.includePeople: only what the site publishes is used: no address is ever guessed or built from a name. People alone (no email or phone) do not make a lead and are not charged.onlyChangedSinceLastRun: the first run delivers every lead and remembers it; later runs deliver only what is new or changed (changeTypeisneworchanged,changedFieldssays what). A site that is blocked or down on a later run keeps its last known state and is not delivered.
Google Maps owner names? People and job titles from team pages
The people list holds the people a business's own website publishes on its team, about or leadership pages. They are not guaranteed to be the owners: an owner or founder appears only when the site names them as such.
With Include the people behind the business on (the default), each row has a people list: {name, title, email, linkedin, sourceUrl} for every person the site publishes as part of the business. Only what the site publishes is used:
- JSON-LD: an Organization's
employeeorfounder, and aPersonthat states ajobTitle. - Team cards on team, about and leadership pages: a repeated block with a name heading and a job-title line. A LinkedIn
/in/link inside a person's card becomes theirlinkedin; onemailto:link in the card becomes theiremail. mailto:links whose text is a person's name.
It never guesses or builds an email address (no first.last@company.com), takes nothing from social networks and does not look people up. Testimonials, blog authors, product cards, speaker line-ups and footer links are left out. The aim is precision: you get fewer people than are on the page rather than wrong ones. People on their own (no email or phone) are listed but are not a lead and are not charged.
Track new leads and changes (change mode)
Turn on Only new or changed leads and run the Actor on a schedule (for example after each weekly Google Maps run). A business is delivered - and charged - only when it is new to the tracker or its state changed: its emails, phones, social links, people (name plus job title) or technology set.
- Save the input as a Task with
"onlyChangedSinceLastRun": true. - Add a Schedule.
- The first run is a baseline: it delivers every lead (
changeType: "baseline") and remembers it. You are charged normally for it. - Later runs deliver only
changeType: "new"or"changed", andchangedFieldslists which ofemails,phones,socials,people,techmoved.
A site that is blocked or down on a later run keeps its last known state and is not delivered. A run where nothing changed succeeds with an empty dataset and charges nothing. Rows removed by your filters never touch the tracker, so a business that starts to match later arrives as new. no-website rows are not delivered in change mode. State is kept in a named key-value store (stateStoreName), so it survives between runs.
Notifications
Fill in one or more destinations in the Notifications section and each run's delivered leads are sent to your chat or phone, for example "New lead: Acme Dental — 2 emails, Shopify", or "Lead update: Acme Dental — emails changed, tech stack changed" for a changed business. Only leads that were delivered and charged are sent, never free rows, never filtered ones, and in change mode never the baseline run. Notifications are free.
Send results to a Google Sheet or CRM
Save the input as a Task, then in the Task's Integrations tab add Google Sheets (appends the dataset's rows to a spreadsheet; the flat columns companyName, domain, primaryEmail, status and the nested objects, which export as JSON text or dotted columns, are easy to filter), or Make, Zapier or n8n to push each lead into a CRM (HubSpot, Pipedrive, Salesforce, Zoho, Airtable, Notion). Map primaryEmail, people[0].name, people[0].title, source.title, domain, phones[0].phone and techStack.ecommerce to the CRM's fields. Or download the dataset as CSV from the run's Storage tab and use the CRM's own import.
Use it as an API
from apify_client import ApifyClientclient = ApifyClient("YOUR_APIFY_TOKEN")run = client.actor("lukehunter/lead-enricher").call(run_input={"datasetId": "WkzbQMuFYuamGv3YF", "requireTech": ["Shopify"], "maxItems": 500})for lead in client.dataset(run["defaultDatasetId"]).iterate_items():print(lead["source"]["title"], lead["primaryEmail"], lead["techStack"]["ecommerce"])
Use with AI agents (MCP)
This Actor works as a tool for Claude, ChatGPT, Cursor and other MCP clients through Apify's MCP server. The only input a call needs is websites (or a datasetId from a Google Maps run).
Claude Desktop (claude_desktop_config.json), with your own token from Apify Console > Settings > API & Integrations:
{"mcpServers": {"apify-leads": {"command": "npx","args": ["-y", "mcp-remote","https://mcp.apify.com?tools=lukehunter/lead-enricher","--header", "Authorization: Bearer ${APIFY_TOKEN}"],"env": { "APIFY_TOKEN": "<YOUR_APIFY_TOKEN>" }}}}
Clients that support OAuth (Claude.ai, VS Code) can connect with just the URL https://mcp.apify.com?tools=lukehunter/lead-enricher and sign in to Apify. Never paste a real token into a shared file or chat.
Example. You ask: "Get me a contact email, the people and the tech stack for plausible.io and mailerlite.com." The agent calls the tool lukehunter--lead-enricher with:
{"websites": ["plausible.io", "mailerlite.com"],"outputFormat": "compact","maxItems": 10}
outputFormat: "compact" returns a small row per business: companyName, domain, primaryEmail, emails (plain addresses, at most 5), phones (at most 3), the socials found, people (name and title, at most 5), a techStack summary (cms, ecommerce, analytics, marketing), leadFlags, status and, for dataset runs, source.title, so the result fits an agent's context. It is charged exactly like the default "full" output: $0.005 per lead with at least one email or phone, nothing for sites where none was found. Set maxItems to cap spend. In change mode, compact rows also keep changeType and changedFields.
Pricing and cost control
Pay per lead, and only for a lead with something in it.
$0.005 per business with at least one email or phone - 1,000 leads = $5.
| Leads | Cost |
|---|---|
| 3 | $0.015 |
| 100 | $0.50 |
| 1,000 | $5.00 |
| 10,000 | $50.00 |
There is no charge for starting a run. One business is one charge, however many emails, phones and people it has. Not charged: a business removed by your filters (it is not delivered at all), a dataset row with no website, a site where nothing was found, a site that blocked the request, was unreachable or disallows reading in robots.txt, people with no email or phone, a business that did not change (change mode), duplicates, and reading the dataset. maxItems is a hard upper bound on cost (at most maxItems x $0.005), and the Actor never delivers a charged row your run's maximum-cost setting cannot pay for.
Limitations
- Only what businesses publish on their own sites. If a site shows no email or phone (many show only a contact form), the row says
no-contacts-found. The Actor does not guess addresses, look people up, verify that a mailbox exists, or search other sites. No email is ever built from a name or a pattern. - Only the pages it reads. By default six pages per site. Contacts on pages that are not linked as contact, about, team, legal, support or locations pages, or beyond the page limit, are not found. Raise
maxPagesPerSite(up to 20). - No logged-in sources. It does not log in anywhere, and it never requests Google Maps or any social network: the dataset is only a list of websites and your own columns.
- robots.txt is respected for every website, page by page; a disallowed page is not read (the reason is in
statusReason). - Sites can block automated requests. If a site answers HTTP 401/403/429, shows a bot check or refuses
robots.txt, the row isblockedand nothing is charged. The Actor does not use proxies, rotate identities, solve CAPTCHAs or try to get around a block. - No JavaScript rendering. Pages that build their content in the browser may show nothing in their HTML; contacts and technologies loaded later by scripts are not found. The tech stack comes from the home page only and from what that page's HTML, headers and cookies reveal, so a tool added only through a tag manager after load can be missed.
- Technology detection is conservative (precision over recall) and limited to the 166 technologies listed above.
- People: precision over recall. A team shown only as photos, in a slider script or an image, or with unusual titles can be missed.
kindis a judgement from the published address: a department mailbox with an unusual name is labelledrole, and a person's address that is neitherfirst.lastnor a common first name nor tied to a named person on the site is labelledroletoo.- Phone numbers come from
tel:links, JSON-LD and clearly labelled text;e164isnullunless the country is clear. - A business whose "website" is a Facebook or Instagram page is a
no-websiterow, not crawled. - Dataset input must be a dataset your account can open (the default dataset of a run in the same account is what an integration passes). At most 20,000 rows are read per run;
maxItemsdefaults to 100, so raise it. - Speed: about one page per second per site and five sites at a time, so 1,000 sites at six pages each take roughly 25-35 minutes.
- This is an independent tool, not affiliated with Google or any company whose website it reads. You are responsible for using the collected data in line with the law and the sites' terms.
Data protection
This Actor only reads what a business publishes itself on its own website, keeps the source page of every contact and person, and respects robots.txt. It does not log in, does not collect personal social profiles (only profile links a company's own site publishes, and a person's LinkedIn link only when their own team card on that site links it), does not guess or build email addresses from names, and does not look anything up outside the sites you give it (the only other lookup is the public DNS MX record of an email's domain). Your dataset is read only to get the list of websites and the columns you chose to copy back; nothing is collected from Google Maps or any other site it came from, and you stay responsible for the lawful basis for that source data too.
The people field and personal email addresses (kind: "person") are personal data under the GDPR and similar laws, even though the business published them. You are responsible for having a lawful basis to use the data: depending on where you and your recipients are, for example a documented legitimate-interest assessment under the GDPR and compliance with CAN-SPAM, PECR/ePrivacy and similar rules before you send marketing email or call. Honour opt-outs and deletion requests, keep a record of where each contact came from (sourceUrl is there for that) and prefer role and generic mailboxes for cold outreach. If you receive a deletion request about data this Actor returned, delete it from your copies.
Use it from your code, Make, Zapier or n8n
Built for automated pipelines: small runs, stable field names, one row per item, and you pay only for rows actually delivered.
One API call that runs the Actor and returns the data (replace $APIFY_TOKEN with your token from Apify Console → Settings → Integrations):
curl -s -X POST "https://api.apify.com/v2/acts/lukehunter~lead-enricher/run-sync-get-dataset-items?token=$APIFY_TOKEN" \-H 'Content-Type: application/json' \-d '{"websites": ["plausible.io", "mailerlite.com", "stumptowncoffee.com"], "maxItems": 20, "maxPagesPerSite": 6}'
JavaScript (npm i apify-client):
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: process.env.APIFY_TOKEN });const run = await client.actor('lukehunter~lead-enricher').call({"websites": ["plausible.io","mailerlite.com","stumptowncoffee.com"],"maxItems": 20,"maxPagesPerSite": 6});const { items } = await client.dataset(run.defaultDatasetId).listItems();
Python (pip install apify-client):
import osfrom apify_client import ApifyClientclient = ApifyClient(os.environ['APIFY_TOKEN'])run = client.actor('lukehunter~lead-enricher').call(run_input={"websites": ["plausible.io", "mailerlite.com", "stumptowncoffee.com"], "maxItems": 20, "maxPagesPerSite": 6})items = client.dataset(run['defaultDatasetId']).list_items().items
Schedules, webhooks and no-code tools: save your input as a Task, add a Schedule (e.g. hourly or daily), and attach a webhook or one of the Integrations tab's connectors (Make, Zapier, n8n, Google Sheets, Slack) to push each run's results into your own system.
Only new items each run: set "onlyChangedSinceLastRun": true on a scheduled Task and every run after the first delivers, and charges for, only items it hasn't delivered before for that same input. That makes it a ready-made change feed.
Reliability: an automated test runs this Actor's parser against the live site every day, and failures are fixed fast.
Feedback: an honest rating on the Store page, good or bad, helps others decide if this Actor fits their needs. Missing a field or found a problem? Open an issue on the Actor page.
FAQ
How is this different from Google Maps scrapers that return contact details?
It enriches any existing results: keep using the Google Maps scraper you already like (or any scraper that gives a website per row) and add this step, instead of re-scraping Google Maps with a different tool. On top of emails and phones it adds the technology stack and the named people with job titles, flags such as runsAds and isEcommerce, and filters (only Shopify stores with an email). And you pay per lead found ($0.005 per business with an email or phone), not per Google Maps place.
Does it scrape Google Maps? No. It reads the website each business already has in your dataset. It never requests Google Maps.
What if a business has no website?
Its row comes back free with status no-website and your source fields (phone, address, rating, ...), so you can follow up another way. Turn this off with includeNoWebsiteRows.
Why did I get fewer rows than my dataset has?
By default only businesses with an email are delivered (onlyWithEmail), dataset rows that share a website are one business, and maxItems caps the run (default 100). The run status says how many were filtered, how many had no website and how many were not read.
Are the emails verified?
No mailbox is contacted. emailDomainHasMx tells you whether the domain accepts mail at all. The emails are the ones the business published on its own site.
Which technologies can the filters use? Any of the 166 listed above. A misspelled name is an input error with suggestions.
Can I run it on a schedule? Yes: turn on change mode (see above) to receive only new or changed leads, optionally with notifications.
Related Actors
Other data tools from the same developer, built to the same standard: official or public sources, hard cost caps, and honest documentation of limits.
- Domain Email Provider Lookup: the email provider (Google Workspace, Microsoft 365, Zoho and more), MX, SPF and DMARC setup of any domain list, with provider-change alerts.
- French Company Leads: French companies (entreprises) from the official Sirene-based API, filtered by NAF code, department, headcount and creation date, with SIREN/SIRET, officers and new-company alerts.
- Poland KRS Company Lookup: Polish companies from the official KRS API by KRS number: NIP, REGON, address, share capital, PKD and the board as published, with optional VAT status.
- Website Contact Scraper: emails, phone numbers and social profile links from any list of company domains, with the page each came from and a changed-contacts-only mode for scheduled runs.
- Impressum Scraper: legal entity data from the imprint of any company website in Germany, Austria and Switzerland: legal name and form, managing directors, HRB/HRA or FN register, VAT id and address, with a changed-data-only mode for scheduled runs.
- Website Tech Stack Detector: the technologies a website runs (CMS, ecommerce platform, analytics, ad pixels, chat, payments, CDN, frameworks) from its home page, with the evidence for each match and a changed-technologies-only mode for scheduled runs.
- Federal Register Rule Effective-Date Radar: final federal rules about to take effect, with citations, dockets and urgency.
- UK Tenders Monitor: new UK public-sector tenders from Contracts Finder and Find a Tender, filtered by CPV code, value, region and buyer, with Slack, Discord and Telegram alerts.