I needed a lead list. Not a thousand names from some reseller database that's been emailed to death. I needed fresh, structured contact data for small businesses across the US, sorted by industry and city, with real phone numbers and working websites.
I built the entire pipeline in one sitting with Claude Code and the Google Places API. It pulled 67,000+ unique businesses in under an hour. Cost me less than $15 in API calls.
Here's exactly how it works and how to build your own.
What you get out of this
The pipeline queries Google's Places API for businesses by category and city. For each result you get back a name, website, phone number, full address, and category type. No scraping HTML. No fighting CAPTCHAs. No getting blocked by Cloudflare.
Then a separate script visits each website to pull contact emails off their /contact and /about pages. Then a CSV builder formats everything for import into whatever outreach tool you use (I use Instantly.ai).
You end up with a CSV that has: email, first name, last name, company name, website, phone, address, city, state, and category. Ready to load and send.
The stack
- Google Places API (New) Text Search endpoint. You send a natural language query like "dentist in Boise Idaho" and get back 20 structured results.
- Node.js Plain scripts. One file per step. No framework.
- Puppeteer Headless browser that visits each business website looking for email addresses.
- Claude Code Wrote every file, tested it, hit dead ends with other approaches, pivoted to Places API, and ran the whole thing.
What you need before starting
A Google Cloud account with the Places API enabled and an API key. Google gives every project $200/month in free credits. The Text Search endpoint costs a few cents per call depending on which fields you request. Each call returns up to 20 businesses.
At those rates, 3,500 API calls (which is roughly what this pipeline makes across all cities and categories) costs well under $15.
The prompt
This is the exact prompt I gave Claude Code:
Build me a Node.js pipeline that uses the Google Places API Text Search
endpoint to find businesses across US cities and export them as a CSV
for Instantly.ai cold email campaigns.
Requirements:
- Search 50+ US cities across CA, NY, FL, TX, ID, OR, WA, CO, AZ, NV,
UT, TN, GA, NC and others
- Categories: dentists, chiropractors, plumbers, electricians, HVAC,
roofers, landscapers, law firms, accountants, restaurants, salons,
auto repair, pet groomers, gyms, daycares, and any other high-value
SMB segments
- Exclude: digital marketing agencies and realtors
- Store results in a JSON database with deduplication by website URL
- Include a Puppeteer-based email finder that visits each site and
scrapes contact/about pages for email addresses
- Output CSV with columns: email, first_name, last_name, company_name,
website, phone, address, city, state, category
- Use env var GOOGLE_PLACES_API_KEY (never hardcode)
- Rate limit at 150-200ms between API calls
- Save progress every 25 queries for resumability
Claude Code tried web scraping first (Google, Bing, YellowPages, DuckDuckGo). Every one got blocked because the session was running from a cloud IP. It pivoted to the Places API on its own and had a working pipeline within minutes. That's the useful part of AI-assisted development here. It hit walls and routed around them without me needing to debug each failure.
The API call
const res = await fetch('https://places.googleapis.com/v1/places:searchText', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Goog-Api-Key': process.env.GOOGLE_PLACES_API_KEY,
'X-Goog-FieldMask': 'places.displayName,places.formattedAddress,places.websiteUri,places.nationalPhoneNumber,places.types'
},
body: JSON.stringify({
textQuery: 'dentist in Boise Idaho',
pageSize: 20,
languageCode: 'en'
})
});
The FieldMask header controls what you get back and what you pay. Only request the fields you actually need. The response is clean JSON with structured data. No HTML parsing required.
Categories I used
I started with the obvious (dentists, plumbers, lawyers) and kept expanding until I'd covered every SMB segment I could think of that has a public-facing website.
Medical: dermatology, orthopedic, mental health, physical therapy, oral surgeons, allergists, cardiologists, sports medicine, acupuncture, massage therapists.
Trades: pool service, fence companies, tree service, carpet cleaning, pressure washing, concrete, painting, drywall, flooring, cabinet makers, solar installers, foundation repair, handymen.
Food and beverage: breweries, wineries, food trucks, juice bars, catering, butcher shops, every type of restaurant.
Fitness: CrossFit, pilates, personal trainers, dance studios, boxing, rock climbing, tennis clubs, trampoline parks.
Professional: architects, interior designers, IT support, bookkeepers, tax preparers, notaries, translators, every type of lawyer.
Education: driving schools, language schools, coding bootcamps, music teachers, pottery studios, summer camps.
Auto: body shops, towing, detailing, RV dealers, motorcycle dealers, transmission repair, custom car shops.
Entertainment: escape rooms, bowling, arcades, comedy clubs, laser tag, go-karts, paintball, museums, live music venues.
I excluded digital marketing agencies (they already know what you're selling) and realtors (totally different buying cycle). Everyone else is fair game.
What I actually got
After running all segments across 50+ cities in 14 states:
- 67,000+ unique businesses with websites
- 97% have phone numbers from Google directly
- 80+ business categories
- About 15-18 new unique leads per API call after deduplication
The 14 states I focused on: CA, NY, FL, TX, ID, OR, WA, CO, AZ, NV, UT, TN, GA, NC. You can add any state or city by editing one array in the code.
The email problem
Google gives you names, websites, and phones. Not emails. If you're doing email outreach, you still need to get those somewhere.
The pipeline includes a Puppeteer script that visits each site and looks for email addresses on /contact, /about, and the homepage. It finds them for maybe 30-40% of leads. For the rest, you can run them through Apollo.io (10K free lookups per month) or Hunter.io ($49/month) to append emails.
The phone numbers are still useful on their own if you're doing calls or SMS alongside email.
Deduplication
You'll hit duplicates. A business in Meridian, Idaho shows up when you query both "dentist in Meridian ID" and "dentist in Boise ID" because Google returns results within a radius, not strictly within city limits.
The database layer catches these by normalizing every URL to its origin and checking against a Set:
function normalizeUrl(u) {
if (!u) return null;
u = u.trim().toLowerCase();
if (!u.startsWith('http')) u = 'https://' + u;
try { return new URL(u).origin; } catch { return null; }
}
export function addBusinesses(db, list) {
const existing = new Set(db.businesses.map(b => b.website));
let added = 0;
for (const biz of list) {
const url = normalizeUrl(biz.website);
if (!url || existing.has(url)) continue;
if (isExcluded(biz)) continue;
db.businesses.push({ ...biz, website: url });
existing.add(url);
added++;
}
return added;
}
Three things happening here:
- Every URL gets stripped to origin only.
https://example.com/contactandhttp://www.example.comboth becomehttps://example.com. That catches most of the near-duplicates. - The Set gives you O(1) lookups instead of scanning the full array for each new lead.
- New URLs get added to the Set immediately, so duplicates within the same batch also get caught.
The exclusion filter checks the business name and category against a short keyword list. If something matches "digital marketing" or "keller williams" it gets dropped before it ever hits the database.
Importing into Instantly.ai
Once you've got your CSV, here's how to get it loaded and sending.
Upload: Go to Lead Management, click Upload Leads, drop your CSV.
Map columns: Instantly asks you to match your CSV headers to its fields. Here's the mapping:
| CSV Column | Instantly Field |
|---|---|
| first_name | First Name |
| last_name | Last Name |
| company_name | Company Name |
| website | Website |
| phone | Phone |
| city | Custom Variable 1 |
| state | Custom Variable 2 |
| category | Custom Variable 3 |
If you have phone-only leads (no email yet), you can still upload them. Use Instantly's built-in enrichment or run them through Apollo first to append emails before activating the campaign.
Assign to a campaign: Pick which campaign these leads belong to. Instantly deduplicates against leads already in your workspace, so if you upload in batches you won't double-send.
Write your email: The custom variables (city, state, category) let you personalize without sounding templated. You can reference their specific industry, their city, or their website directly in the email body using , , ``, etc.
Whatever you're selling, the first email should give them something useful before asking for anything. A free audit result, a specific observation about their site, a relevant stat about their industry. Make it about them, not about you.
Warm up first: If you're sending from new domains, warm them for at least 14 days before scaling. Instantly handles this automatically. Start at 20-30 sends per mailbox per day and scale to 50-80 once warmup is done. Don't skip this step or you'll land in spam on day one.
Run it yourself
Five files:
scrape-businesses.mjsGoogle Places API scraperfind-emails.mjsPuppeteer email finderbuild-csv.mjsCSV formatterdb.mjsJSON database with deduplicationsegment-runner.mjsRun specific industry segments one at a time
Set your GOOGLE_PLACES_API_KEY environment variable, run node scrape-businesses.mjs, wait about 10 minutes, then run node build-csv.mjs. That's your lead list.
If you want to run one industry at a time (useful for testing or if you only care about certain verticals), use the segment runner: node segment-runner.mjs medical or node segment-runner.mjs trades.
This post describes a technical process, not legal or compliance advice. Every platform you touch (Google Cloud, Instantly, Apollo, your email provider) has its own terms of service and acceptable use policies. Read them. Adapt your approach to stay within those terms. Anti-spam laws (CAN-SPAM, GDPR, CASL, state privacy statutes) vary by jurisdiction and change regularly. What's described here worked for my circumstances. Yours may differ. Do your own due diligence before sending anything.
Fact-check notes and sources
- Google Places API pricing: $200/month free credit confirmed as of April 2026. Text Search (Pro tier) is billed per session at varying rates depending on fields requested. Google Maps Platform Pricing
- Google Places API returns a maximum of 20 results per Text Search request as documented in the Places API reference.
- ADA Title III lawsuit data referenced for context: 4,605 federal website accessibility lawsuits filed in 2023 per UsableNet's annual report.