How to Scrape Google Maps (Python Guide)
- The Maps front end is JS-only and gated by a consent wall, so requests + BeautifulSoup on the map page returns 0 places.
- I hit Google's internal Maps data endpoint with a
SOCSconsent cookie on June 12, 2026: HTTP 200, 20 real Austin coffee shops parsed (name, rating, category, address). - That endpoint is undocumented and brittle: field positions shift, and a single IP gets rate-limited and CAPTCHA-walled within a few hundred queries.
- For stable data at scale, use the official Places API (priced per 1,000 calls) or a scraper API like ChocoData that returns JSON and rotates IPs.
Google Maps is the largest local-business dataset on the web, and the fields people want (name, rating, category, address, phone) drive lead lists, market maps, and competitor research. This guide builds a real Python scraper, tests it against live listings, and shows exactly where DIY works and where it breaks. I ran the code on June 12, 2026 and pasted the real output below, including the part most tutorials skip: the front-end page returns nothing, and the data lives on an internal endpoint that Google can change without notice. For the fundamentals, see my web scraping guide and the Python walkthrough.
Can you scrape Google Maps, and is it legal?
You can fetch public Maps data with a script, and the legality depends on the method and what you keep. The technical reality first: the visible Maps app at google.com/maps runs entirely on JavaScript, so a plain request gets a shell, not listings. The data is reachable through Google’s own internal endpoint, which I test below. The legal part has two layers worth separating.
On US computer-access law, reading pages that anyone can load without logging in sits on the safer side. In hiQ Labs v. LinkedIn, the Ninth Circuit held that scraping public data does not violate the Computer Fraud and Abuse Act, and the Supreme Court’s Van Buren decision narrowed the CFAA toward traditional hacking. Querying a public Maps endpoint with a bot is not, by itself, a federal computer crime.
Contract law is the live risk. Google’s Terms of Service and the Maps/Google Earth Additional Terms restrict automated access and scraping of the service. hiQ is the cautionary tale: it won the CFAA question and still paid a $500,000 judgment for breaching LinkedIn’s user agreement. Two rules keep you on the lower-risk path: stay on public results, never script a logged-in account, and treat business data (not personal profiles) as your target so you stay clear of the GDPR. I am a tester, not a lawyer, so get advice before any commercial scrape. My is web scraping legal pillar covers the full picture.
What data can you extract from Google Maps?
A Maps search response carries most of the commercially useful business fields, plus a few that load separately. Knowing the structure tells you what to target and what an API should hand back as JSON.
| Field | Where it lives | Reliable from a search call? |
|---|---|---|
| Business name | Search response | Yes |
| Average rating | Search response | Yes |
| Category | Search response | Yes |
| Full address | Search response | Yes |
| Latitude / longitude | Search response | Yes |
| Review count | Search response (position varies) | Inconsistent |
| Phone / website | Place details | Separate call |
| Opening hours | Place details | Separate call |
| Review text | Reviews endpoint (paginated) | Separate call |
| Photos | Photos endpoint | Separate call |
The pattern to internalize: the list-level fields (name, rating, category, address) come back in one response, and the deeper fields (phone, hours, full reviews) require a follow-up request per place. That ordering decides how many calls a full scrape costs.
How does Google Maps block scrapers?
Maps stacks four defenses, and the first two stop a naive scraper before it sees any data.
- Consent wall. A request without a consent cookie redirects to
consent.google.com. I confirmed this: fetchinggoogle.com/maps/search/...returned HTTP 200 but the final URL wasconsent.google.com/m?continue=...and the body was the consent UI, not map data. - JS-only rendering. Even past consent, the visible app injects an
APP_INITIALIZATION_STATEblob that is the map scaffold, not the result list. The listings arrive over a later XHR call. A static parse of the page finds zero business names. - IP rate limits. A single IP firing repeated map queries gets throttled, then served a
/sorry/CAPTCHA interstitial. Residential rotation is the only durable fix at volume. - Protobuf payloads. The data endpoint returns a
)]}'-prefixed nested array (protobuf-style), with no field names. You parse by numeric index, and Google reshuffles those indexes periodically.
Method 1: scrape Google Maps with Python (requests)
The visible Maps page is a dead end for requests, so the working DIY route is Google’s internal Maps data endpoint (/search?tbm=map&pb=...), unlocked with a SOCS consent cookie. This is the same endpoint the Maps front end calls. It is undocumented, so treat it as fragile. Here is the exact code I ran:
# -*- coding: utf-8 -*-
import requests, json
HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/126.0.0.0 Safari/537.36",
"Accept-Language": "en-US,en;q=0.9",
}
def get(seq, *idx):
"""Safe nested index into Google's positional protobuf array."""
cur = seq
for i in idx:
if not isinstance(cur, list) or len(cur) <= i:
return None
cur = cur[i]
return cur
def scrape(query, lat, lng):
s = requests.Session()
s.headers.update(HEADERS)
# SOCS cookie clears the consent wall without a redirect.
s.cookies.set("SOCS", "CAESEwgDEgk0ODE3Nzk3MjQaAmVuIAEaBgiA_LyaBg",
domain=".google.com")
pb = (f"!4m12!1m3!1d10000!2d{lng}!3d{lat}!2m3!1f0!2f0!3f0"
f"!3m2!1i1024!2i768!4f13.1!7i20!10b1!12m3!2m2!1i392!2i106")
params = {"tbm": "map", "hl": "en", "gl": "us", "pb": pb, "q": query}
r = s.get("https://www.google.com/search", params=params, timeout=30)
r.raise_for_status()
# Strip Google's anti-JSON-hijack prefix, then parse.
body = r.text[4:] if r.text.startswith(")]}'") else r.text
results = json.loads(body)[0][1]
places = []
for e in results:
info = get(e, 14) # each result's detail array
name = get(info, 11)
if not name:
continue
places.append({
"name": name,
"rating": get(info, 4, 7), # rating sits at index 7 of the rating array
"category": get(info, 13, 0),
"address": get(info, 39),
})
return r.status_code, places
status, places = scrape("coffee shops in Austin TX", 30.27, -97.74)
print("HTTP", status, "-", len(places), "places\n")
for p in places[:8]:
print(f'{p["name"][:35]:<36}{p["rating"]} {p["category"]} {p["address"]}')
The pb string is the protobuf-encoded viewport: 2d/3d carry longitude and latitude, and 7i20 requests 20 results. I ran the script and pasted the real output:
HTTP 200 - 20 places
Black Fox Coffee 4.6 Coffee shop 323 W 6th St, Austin, TX 78701
Trippy Buck Coffee 4.9 Coffee shop 720 Brazos St, Austin, TX 78701
Terrible Love 4.9 Coffee shop 3908 Avenue B, Austin, TX 78751
Klerje Coffee 4.8 Coffee shop 1614 E 6th St Apt 112, Austin, TX 78702
Café Crème - Downtown 4.8 Coffee shop 710 W Cesar Chavez St, Austin, TX 78701
Comunidad Specialty Coffee & Cold P 4.9 Coffee shop 1008 E 6th St, Austin, TX 78702
Idlewild Coffee 4.8 Coffee shop 1400 Lavaca St, Austin, TX 78701
Lone Man Coffee 4.9 Coffee shop 3111 W 35th St, Austin, TX 78703
Twenty real Austin coffee shops, with name, rating, category, and address, from one unauthenticated GET. Three honest caveats from my run:
- The result order changes between runs. Google reranks, so the same query returned the same 20 places in a different sequence on a repeat call. Sort client-side if you need stability.
- Review count is unreliable by index. The rating sub-array came back as
[null, ..., 4.8], so the rating parses cleanly but the review-count position was empty in my response. I left it out rather than print a guessed field. Index positions in this payload shift, and parsing extra fields means re-checking them against fresh responses. - It will not survive volume. This is one IP and one query. Loop it a few hundred times and Google serves the
/sorry/CAPTCHA page instead of JSON. The script has no proxy rotation and no CAPTCHA handling, by design, so you see the raw behavior.
This works today and is genuinely useful for a one-off pull of a few queries. For BeautifulSoup fans: there is no HTML to parse here, the payload is JSON, so bs4 does not enter the picture for Maps. If you would rather drive the real UI, Playwright can click through the consent dialog and read rendered cards, at the cost of running a browser per query.
Method 2: the official Places API and scraper APIs
If you need Maps data that does not break next month, two routes return clean JSON without index spelunking. The first is Google’s own API; the second is a scraper API that handles the blocking for you.
Google Places API (New). This is the lowest-risk source because it is Google sanctioning the access. It returns business data as structured JSON and is the right call when terms-of-service exposure matters. The trade-offs are per-call billing and a cap on results per query. Current pricing from Google’s billing page:
| SKU (Places API New) | Free monthly allowance | Price per 1,000 (100k-500k tier) |
|---|---|---|
| Text Search (Pro) | 5,000 calls | $32.00 |
| Nearby Search (Pro) | 5,000 calls | $32.00 |
| Place Details (Pro) | 5,000 calls | $17.00 |
| Place Details (Essentials) | 10,000 calls | $5.00 |
For a few thousand lookups a month the free allowance can cover you. The catch for bulk list-building: Text Search returns a paginated, capped set per query, so harvesting every business in a large area takes many calls and adds up.
Scraper API. A scraper API turns the consent wall, JS rendering, IP rotation, and CAPTCHA solving into one HTTP call that returns JSON. ChocoData (chocodata.com) is one example I checked against its own docs. It exposes a universal endpoint (“one endpoint, any site”) plus 453 dedicated endpoints. The documented request shape:
import requests
API_KEY = "YOUR_API_KEY" # free tier: 1,000 requests/month, no card
resp = requests.get(
"https://chocodata.com/api/v1/google/maps",
params={"q": "coffee shops in Austin TX", "api_key": API_KEY},
timeout=60,
)
data = resp.json() # JSON in, JSON out
for place in data.get("places", []):
print(place["name"], place["rating"], place["address"])
The endpoint pattern is GET /api/v1/{site}/{resource}?api_key=..., so swapping the path targets other sites. Per ChocoData’s site, the service handles residential proxy rotation, CAPTCHAs, retries, and JavaScript rendering, and returns structured JSON. I have not independently benchmarked their success rate or response schema, so treat the field names above as illustrative and verify against their docs on the free tier.
How the three methods compare on what actually matters:
| Factor | DIY (internal endpoint) | Places API (official) | Scraper API |
|---|---|---|---|
| Returns data today | Yes (I tested it) | Yes | Yes (vendor-handled) |
| Survives field changes | No (positional, brittle) | Yes (named JSON) | Vendor’s problem |
| Proxies / IP rotation | You build it | Not needed | Included |
| CAPTCHA handling | None | Not needed | Included |
| Terms-of-service risk | Higher (unofficial) | Lowest (sanctioned) | Medium (still scraping) |
| Cost model | Free + your proxies | Per call | Per request |
ChocoData’s published pricing: a free tier of 1,000 requests/month with no card, paid plans from $19/month (27,000 requests), and pay-as-you-go top-ups at $0.90 per 1,000 successful requests. Against Places API Text Search at $32 per 1,000 after the free allowance, a scraper API can be cheaper for high-volume list-building, while the official API wins when you need Google’s blessing on the access.
Which method should you choose?
Pick based on volume, stability needs, and risk tolerance. Here is my decision rule.
- Learning or a one-off under ~100 queries: use the Method 1 script. It returned 20 real places per query in my test and costs nothing. Accept that it can break when Google reshuffles the payload.
- Compliance-sensitive or commercial product: use the official Places API. It bills per call but it is the sanctioned path, returns named JSON, and will not break on a layout change.
- High-volume list-building across many areas: use a scraper API. It removes the proxy and CAPTCHA line items and the per-call cap, at the cost of still being unofficial access.
The deciding fact from my run: a plain request to the Maps page returns 0 listings, the internal endpoint returns 20 but is positional and rate-limited, and closing the gap to reliable bulk data means either paying Google per call or letting an API own the proxy pool and the CAPTCHA solver. For most readers past a handful of queries, that is work an API already did.
FAQ
Yes. The Places API (New) returns business data as JSON. Text Search on the Pro tier is $32 per 1,000 calls after a 5,000-call free monthly allowance; Place Details Pro is $17 per 1,000. It is the lowest-risk source, but it bills per call and caps results per query.
google.com/maps redirects no-cookie clients to a consent interstitial, and even past that the place data loads over a later XHR call as nested protobuf, not in the initial HTML. BeautifulSoup parses the shell and finds zero business names.
Review text loads through a separate paginated endpoint, not the search response. The search call gives you the place list and aggregate rating; pulling full review bodies means following the per-place reviews endpoint or using an API that does it for you.