To add emails to a Google Maps business list, use two Apify Actors: the Google Maps Scraper collects the places and websites, then the Website Email Scraper checks those websites for published contacts. Match the website results back to the original Place IDs to produce an enriched CSV.
This workflow is useful when you want to retain the whole business list, including places without websites or email matches. If you already have a Maps CSV, start from that file and go straight to the website preparation step.
Step 1. Export the businesses and keep a master copy
Open Google Maps Scraper on Apify, click Try for free and use its Input form in Apify Console. Start with one category and city, for example 20 bike repair shops in Portland. The full Maps business export guide walks through that first run.
{
"searchStringsArray": ["bike repair shop"],
"locationQuery": "Portland, Oregon, USA",
"maxCrawledPlacesPerSearch": 20,
"website": "allPlaces",
"language": "en"
}Download the Dataset as CSV. Keep placeId, title, address, website and the Maps url. Add your own source_row value so every original row has a stable reference even when an identifier is missing.
Save an untouched original and create a working sheet called Places. Retain rows with no website. Their eventual status will be “no listed website,” which is useful information about your source list.
Step 2. Prepare website inputs without losing the place mapping
Copy the nonempty website values into a second sheet called Crawl inputs. Give each distinct input a crawl_key and copy that key back to the relevant Places rows. The key connects your Maps file to the website results.
- Remove leading and trailing spaces from the copied URLs.
- Keep the original website value alongside the cleaned input.
- Deduplicate identical website inputs so you can reuse one crawl result for several places.
- Preserve meaningful branch paths when the URL identifies a location.
- Keep a mapping from every
source_rowandplaceIdto itscrawl_key.
For example, two shops might both list https://northstar.example/. Crawl that input once and map it back to both places. But /locations/north and /locations/south can contain different local contacts. Keep those full URLs as separate inputs when the branch matters.
The Website Actor accepts domains and full URLs, including contact and location pages. Do not reduce every input to its root domain: doing so can discard the page that makes a contact relevant to a particular branch.
Step 3. Run the websites through the email Actor
Open the Website Email Scraper on Apify, click Try for free and open its Input form in Apify Console. Start with five or ten real URLs from your prepared list.
- Paste the URLs into Domains or website URLs, one item per website input.
- Choose
allWebsitesfor Result mode. This retains diagnostic rows for websites without email as well as successful matches. - Set Maximum saved results at least as high as the number of distinct inputs in the test.
- Use three pages per website for the first pass.
- Keep Same-domain crawl only on and JavaScript-rendered contact checking enabled.
- Choose
includePersonalData: falseif you mainly need generic company inboxes. - Review current pricing, set a run budget and click Start.
The input reference describes these controls. The screenshot below shows the field reference; paste your own website list into the editable Console form.

JSON template for a two-website test
Replace both placeholders with real URLs from your Maps CSV. This example saves up to two website records and checks up to three pages per input.
{
"domains": [
"https://YOUR_FIRST_BUSINESS_WEBSITE",
"https://YOUR_SECOND_BUSINESS_WEBSITE"
],
"maxResults": 2,
"resultMode": "allWebsites",
"maxPagesPerWebsite": 3,
"includePersonalData": false,
"sameDomainOnly": true,
"renderJavaScript": true
}Here, maxResults caps saved website records, not individual emails. One website row can contain several addresses. In allWebsites mode, no-contact and unsuccessful website records can also be saved and billed. This mode helps you explain what happened to each crawl input.
Step 4. Keep the contact and crawl evidence
Download the website Dataset as JSON as well as CSV if you need the nested email evidence. Match the returned input to the URL in your Crawl inputs sheet. Keep the original input even when the Actor also returns a final URL or a recovered start page.
| Field | How it helps |
|---|---|
input, url, domain | Match the result to your submitted website input. |
status, pagesFetched | Distinguish a completed scan from a failed or incomplete one. |
emails, emailDetails | Keep addresses with their source URL and domain evidence. |
fetchedUrls, pageDiagnostics | See which pages were actually reached. |
scanLimitations, errors | Explain missing contacts and choose targeted retries. |
Create a Contacts sheet with one row per crawl_key and email, including sourceUrl and an optional scope label. The Actor can follow a branch page to the company homepage, so a found email may be company-wide. Mark it company until the source page confirms a branch-specific contact.
Step 5. Join the results back to every original place
Use Places as the left side of the join. In a spreadsheet, use an exact lookup from its crawl_key to one summary row per key in Crawl inputs. Bring back the crawl status and a combined email list. A missing lookup should remain visible for investigation.
Keep multiple email rows in Contacts rather than multiplying the Places table. If you need an expanded contact export, create it separately and count distinct Place IDs when reporting business coverage.
| Original place | Website input | Enrichment result |
|---|---|---|
| North branch | Shared company homepage | Email found; company-level scope. |
| South branch | Same company homepage | Reuse the crawl; branch contact still unconfirmed. |
| Independent shop | No listed website | Retained with no-listed-website status. |
| Another shop | Its business website | Completed scan, no email found within the page limit. |
Check that the final Places sheet still contains the same number of original rows. The raw Maps CSV remains your reference. Every email should lead back through its crawl key to the website input and source page.
Handle blocked sites, redirects and no-contact results
A fetched_no_contacts result means a bounded scan reached pages without finding the requested contacts. It does not establish that the entire website has no email. Read pagesFetched and scanLimitations before choosing a retry.
For blocked, incomplete or failed results, inspect diagnostics. JavaScript checking is enabled in the example: the Actor starts with HTTP and can use a browser fallback for incomplete or no-contact pages. Review extractionMethod and browserStatus when those fields are returned.
With same-domain protection enabled, a cross-domain redirect can be rejected for extraction and recorded in rejectedRedirectUrls. Verify whether the business has moved to a new domain. If confirmed, add the verified destination as a new crawl input and retain the old-to-new mapping.
For promising sites where the contact page was missed, try a higher maxPagesPerWebsite on a small subset; the supported range is 1–25. Retry those existing keys and update their results. Keep failed, completed-no-contact and not-attempted inputs distinct.
Process larger lists and measure coverage honestly
Split large website lists into manageable batches. Set each batch’s maxResults high enough for its distinct inputs, save its run and Dataset IDs, and check RUN_SUMMARY for processed, saved, failed and filtered counts, charge limits and stop reason.
Report the original places, places with a website, distinct crawl inputs, completed scans and places with usable contacts. For example, four confirmed matches out of 12 original places mean contact coverage of four out of 12. Seven unique website inputs are a separate crawl-work count, even when some are shared by branches.
Let your agent coordinate both Actors through Apify MCP
Connect an MCP-compatible agent to https://mcp.apify.com and authorize Apify using the MCP setup guide. Supply your existing CSV or ask the agent to collect the places first. It should keep the mapping between the two stages.
Enrich my Google Maps business list with website emails.
Use x_guru/google-maps-scraper if discovery is needed,
then x_guru/website-email-phone-finder for the websites.
Category and city, if needed: [MY_SEARCH]
First discovery limit: 20 places.
Approved budget per Actor run in USD: [AMOUNTS]
Inspect both current schemas. Preserve every original place
with source_row, placeId, Maps URL and original website.
Prepare unique crawl inputs with crawl_key, preserving
meaningful branch paths and the mapping to source rows.
Use allWebsites, a maxResults cap covering the input count,
three pages per website, sameDomainOnly=true and
renderJavaScript=true. Apply each approved spending cap
using callOptions.maxTotalChargeUsd.
Read all Dataset pages and check each RUN_SUMMARY.
Left-join one crawl summary back to every original place.
Save Places and Contacts separately. Keep each email's
source URL and company-or-branch scope when established.
Report missing websites, failed or partial crawls, no-contact
scans and matched places. Include all run and Dataset IDs.The agent can start each Actor with call-actor, wait with get-actor-run and page through results with get-dataset-items. For the website stage alone, see the website email guide. For an application-based setup, follow the Apify run and export guide.



