Google Maps lead generation: a practical playbook
A category and a city list, turned into a filtered, enriched CSV of leads. Full Python loop and the credit cost of a 100-city campaign, August 2026.

Every local lead list starts the same way: a category, a set of cities, and a filter that tells you who is actually worth calling. This is the end-to-end version of that workflow, from query to CSV, with the credit cost worked out for a real campaign size.
If you want the endpoint reference itself, parameters, response fields, what a single search costs, that’s covered in how to scrape Google Maps results with Python. This post is the playbook built on top of it: how to turn Maps results into a list you’d hand to a sales rep.
Step 1: define the ICP before you write a query
“Businesses in Austin” is not a filter. A usable ICP for local outreach needs three things nailed down before you run a single request:
- Category. The search term you’ll pair with each city: “plumber”, “dentist”, “hvac contractor”. Test it manually on Maps first and check it doesn’t pull in adjacent categories you don’t want.
- A traction floor. A minimum review count (
reviews) tells you the business is established enough to be worth contacting, not a listing that opened last month. - A pain signal. For a web agency, that’s a missing
websitefield. For a booking-tool pitch, it might be atypethat implies appointment-based work. The signal is whatever your product solves.
Everything downstream is just executing that definition at scale.
Step 2: query category and city, paginated
Loop the category across your city list, and treat each page as a separate billable request:
import requests
API_KEY = "faf_live_..." # your FastAF key
def maps_search(query, page=0):
resp = requests.get(
"https://api.fastaf.co/v1/maps",
params={"query": query, "page": page},
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=60,
)
resp.raise_for_status()
return resp.json().get("search_results", [])
Two pages per city (roughly 40 places, since a Maps page runs about 20 results) is usually enough for mid-size cities. Dense metros may need three or four before you hit diminishing returns.
Step 3: filter on the fields, not on judgment
This is where the ICP from step 1 becomes code. No manual review of the raw list:
CATEGORY = "plumber"
CITIES = ["Austin TX", "Tampa FL", "Boise ID"] # your 100-city list
MIN_REVIEWS = 25
MIN_RATING = 4.0
PAGES_PER_CITY = 2
leads = []
for city in CITIES:
for page in range(PAGES_PER_CITY):
for place in maps_search(f"{CATEGORY} in {city}", page=page):
if place.get("reviews", 0) < MIN_REVIEWS:
continue
if place.get("rating", 0) < MIN_RATING:
continue
if place.get("website"):
continue # has a site already, not the ICP for a web-agency pitch
leads.append(place)
print(f"{len(leads)} raw leads from {len(CITIES)} cities")
Flip the logic for a different pitch: rating < 4.0 and reviews > 50 finds businesses with real traffic and a reputation problem, a different but equally common ICP.
Step 4: enrich the shortlist, not the whole pull
Don’t re-fetch every place you scraped, only the ones that survived the filter. Every result carries a place_id, which you can look up again right before a lead goes to a rep, so the record is current rather than however many days old the bulk pull was:
def maps_lookup(place_id):
resp = requests.get(
"https://api.fastaf.co/v1/maps",
params={"place_id": place_id},
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=60,
)
resp.raise_for_status()
results = resp.json().get("search_results", [])
return results[0] if results else {}
enriched = [maps_lookup(lead["place_id"]) if lead.get("place_id") else lead for lead in leads]
This is also the cheaper order of operations: filtering first means you pay the enrichment cost only on leads you’ll actually use, not on every place in every city.
Step 5: export the CSV
import csv
with open("leads.csv", "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=["title", "phone", "address", "rating", "reviews", "type"])
writer.writeheader()
for lead in enriched:
writer.writerow({k: lead.get(k, "") for k in writer.fieldnames})
That’s a file you can drop straight into a CRM import or a cold outreach tool.
What does a 100-city campaign cost?
A /maps request is 5 credits regardless of query. At 2 pages per city:
| Step | Requests | Credits |
|---|---|---|
| Discovery: 100 cities x 2 pages | 200 | 1,000 |
| Enrichment: ~150 shortlisted leads | 150 | 750 |
| Total | 350 | 1,750 |
On the Growth pack ($29 for 75,000 credits), 1,750 credits is roughly $0.68 worth of the pack. If you’re testing the category and filter first, the free tier is 500 credits with no card, enough to run discovery and enrichment on 20 to 25 cities before deciding the ICP is right. Failed requests aren’t billed, credits are reserved when a request starts and returned if nothing comes back, so a bad city query doesn’t cost you anything beyond the wasted call.
The short version
Nail the ICP down to a category, a traction floor and a pain signal before writing any code. Query wide, filter hard, and only spend enrichment credits on the leads that already passed the filter. A full 100-city sweep with enrichment lands under 2,000 credits, small enough that the constraint on a campaign like this is never the API bill, it’s how good the filter is.