Google Maps Scraper API: Extract Business Data at Scale
A Google Maps scraper API turns a keyword and a city into business records as JSON: name, address, phone, website, rating, opening hours and category. Below: the fields the public listing exposes and the two that need care, how deep pages drift and how the API stops them, what a record costs in credits, and the rules that decide whether the service is still answering next year.
What a Google Maps scraper API does, and what it does not
The map listing is the most complete public directory of local businesses in most countries, and it is built for a person with a browser, not for a program that needs a thousand rows.
A scraper API sits in between: you send a keyword and a location, the service fetches the public results and returns the records in one JSON shape. Session handling, proxies and parser breakage are its problem, not yours.
It does not log in, does not read anything behind an account and, done properly, does not fight anti-bot systems. It reads what a logged-out visitor sees, more slowly than that visitor would, and a challenge page or a 429 ends the request rather than triggering a retry from another address.
Which fields Google Maps data extraction can return
The public listing exposes the same core fields for almost every business, and a few more that only appear when you open a single place. Locomint maps both onto one Place record, so a details response is a superset of the search summary with the same field names.
| Field | What it holds | Available from |
|---|---|---|
place_id | Stable identifier, shared with official sources; use it to diff runs | Search |
name, categories[] | As listed, localised by the language parameter | Search |
address | full, street, city, region, postal_code, country_code | Search |
location | lat, lng | Search |
phone, phone_e164, website | phone as displayed locally, phone_e164 normalised (+351…); website exactly as listed | Search |
rating, review_count | Star average and review total | Search |
status | open, temporarily_closed, permanently_closed | Search |
hours | All seven days as lists of { open, close } in HH:MM | Details |
description, plus_code, timezone, photo_urls[], photos_count, attributes{} | The extra fields shown on a single listing; attributes maps attribute name to true/false | Details |
enrichment | Emails, socials, WhatsApp and website status read from the business website | Details |
Two fields need care. A business that works from a van or a home office has no storefront, so its record carries service_area_business: true and an empty address.full, with country_code inferred from the phone number where that is unambiguous. And price_level exists in the schema but is rarely populated by the source, so a filter that depends on it will drop almost everything.
How to scrape Google Maps with an API: your first request
Send a keyword in q, a place in location, and an API key in the X-API-Key header. Always include the country in the location. During our own canary runs "Warsaw" on its own resolved to Warsaw, Indiana; "Warsaw, Poland" cannot.
curl "https://api.locomint.io/v1/places/search?q=coffee%20shop&location=Lisbon,%20Portugal&limit=10" \
-H "X-API-Key: $LOCOMINT_KEY"
The response has three parts: data, a list of place summaries; pagination, with page, limit, returned and next_page (null on the last page); and meta, which carries the request_id, credits_used, latency_ms, the source that answered and the geocoded_location the search resolved to. Check it once per new city to catch the wrong Warsaw before paying for ten pages of it. Search costs one credit per place returned, so the request above costs at most ten.
The same request in Python, using httpx, and then a second call that fetches the full record for the first result:
import httpx
API = "https://api.locomint.io/v1"
headers = {"X-API-Key": "lm_free_your_key_here"}
with httpx.Client(base_url=API, headers=headers, timeout=30) as client:
search = client.get("/places/search", params={
"q": "coffee shop",
"location": "Lisbon, Portugal",
"limit": 10,
}).json()
for place in search["data"]:
print(place["place_id"], place["name"], place.get("rating"), place.get("website"))
first = search["data"][0]["place_id"]
full = client.get(f"/places/{first}").json()
print(full["hours"])
print(full.get("enrichment"))
The search reference lists the remaining parameters: page for the next page, language (BCP-47, default en) for localised names and categories, and lat, lng and radius (metres, default 5000) when you would rather anchor a search on coordinates than on a place name.
Paging, limits and getting every business in a city
Set limit=50 and increase page until pagination.next_page is null. Two things happen on deep pages. Consecutive pages overlap slightly, so keep a set of place_id values and skip the ones you have seen. And the source drifts: page four of "restaurant in Lagos, Nigeria" returned Lagos, Portugal in our tests. Locomint checks every page against the area the search resolved to and ends paging when a page falls outside it, so you get a clean stop rather than a slow leak of foreign records.
One broad query therefore will not cover a large city. Run the same keyword against each district, or anchor searches on a coordinate grid with a small radius, and merge on place_id; the guide to scraping Google Maps by city without missing businesses gives the procedure.
Size a broad query first. GET /v1/places/count takes the same q and location, costs nothing, and returns count with either exact: true (the first page was not full) or at_least: true (a full page came back; a search will find more). Each key gets 50 fresh counts a day; repeats within 24 hours are served from cache and answer cached: true.
The Free plan allows 60 requests a minute; Starter, Growth and Scale allow 120, 300 and 600. X-RateLimit-Remaining on each response says how many are left in the window, and a 429 carries Retry-After; sleep for that long; failed requests count too. Quota is reserved before any work is done, so a request that would exceed your monthly credits is refused with 402 and costs nothing.
Filter on the summary before you fetch details. The search result already carries website, rating, review_count and status. If you only want open businesses with a website and at least four stars, apply that filter to the search page and call details only for the survivors. A 50-place page costs 50 credits; details on the 20 that pass cost 20 more, not 50.
Website enrichment: the contact fields the map never shows
The listing has a phone number and a website, but no email address, no social profiles and no WhatsApp number. Those live on the business website. Locomint reads them inside the details call: on the first request for a place that lists a website, it fetches the homepage, follows one link to the contact page when the homepage gives no email or WhatsApp, and fills an enrichment block.
enrichment.emails: role mailboxes on the company domain, such as info@ or bookings@. Person-named addresses are dropped before the response is built.enrichment.socials: profile links found on the site, keyed by network.enrichment.whatsapp: the number behind a click-to-chat link, when the site has one.enrichment.website_status:ok,unreachable,parked,redirect_socialorno_website, so you can tell an empty result from a failed crawl.
Enrichment is cached per website for 30 days and is included in the one credit a details call costs. Pass refresh=true to re-read the site now for one extra credit. Two limits: the crawler does not execute JavaScript, so a contact page that renders client-side yields nothing; and a site that answers the first request with a 403 is left alone and reported as unreachable, never retried through another route. When you already have the websites, the extraction endpoint does the same read on up to 20 URLs per call.
The rules a responsible business data API follows
A scraper's operating rules decide whether it is still answering in a year. These are ours; ask any vendor you consider for theirs.
- Public, logged-out pages only. No accounts, no session cookies from a signed-in user, no private data.
- Conservative rate limits and caching. The source is fetched no faster than a patient person browses, and repeated answers come from cache.
- Stop on any block. A challenge page or a rate-limit response ends the request; it is never retried through another route.
- Business contact points only. Role mailboxes and company profiles are kept; owner names and personal mailboxes are not stored or sold.
- A removal path that works. Any business can ask through the removal form; removals take effect across the API within seven days.
Whether collecting public business data is lawful depends on where you are, what you collect and what you do with it; this is not legal advice. The article on whether web scraping is legal covers the questions to ask, and the terms of service say what the API may be used for.
What it costs to extract business data at scale
Locomint bills in credits with a fixed price per operation. There are no add-ons for enrichment or extra fields; the details call returns everything it has. GET /v1/usage is free and returns credits_used and credits_remaining, so a long job can check before each page instead of waiting for a 402.
| Operation | Credits |
|---|---|
| Search, per place returned | 1 |
| Details, including enrichment | 1 |
Details with refresh=true | 2 |
| Reviews, per 10 reviews | 1 |
| Email verification, per address | 1 |
| Website extraction, per page | 1 |
500 dentists in one city with full details is 500 search credits plus 500 details credits. The free plan's 200 credits a month, no card, cover one category in one city end to end; Starter ($2.99), Basic ($9), Growth ($29) and Scale ($79) raise the monthly credits, and the pricing page lists how many each includes.
Where to start this week
Choose one category and one city you know well. Run a search with limit=50, check meta.geocoded_location, and count how many summaries have a website and a rating. Fetch details for ten of them and compare the hours and the enrichment block against the real websites. That is about 60 credits and ten minutes.
Then decide what the records are for. A directory or a market study needs the same query repeated on a schedule, keyed on place_id, so you can see which businesses appeared, went permanently_closed or changed rating between runs. The status page shows the daily canary success rate and latency for the source; read it before you schedule anything.
Frequently asked questions
Is a Google Maps scraper API the same as the official Places API?
No. The official Places API is Google's own product, billed per request by SKU, and its terms limit how long you may store most of what it returns. A scraper API reads the public listing pages instead and returns records you can keep in your own database. Locomint uses the same place identifiers as the official API, so a record from either path refers to the same business.
Can I get reviews as well as the business record?
Yes. GET /v1/places/{place_id}/reviews returns the latest page of reviews, about ten, at one credit per ten reviews; reviewer names are omitted unless you pass include_author=true. Only the latest page is available, because paging further would mean working around a protection measure, which Locomint does not do.
What happens when the source is blocked or slow?
The API answers 503 with a source_unavailable error and a Retry-After header, and nothing is charged. A daily canary run publishes the current success rate and latency on the public status page, so you can check whether a failure is on your side or ours before retrying.