To audit what customers actually see on Google Maps, look up each location's Place ID with Places API (New) Place Details, request only the fields you care about with a field mask, and compare phone, website, business status, and hours against your own records. Flag every mismatch for a human to fix in Google Business Profile.
Updated September 2026.
The Business Profile APIs tell you what you submitted and, through getGoogleUpdated, where Google's version differs. The Places API answers a simpler question from the outside: what does the public place record say right now? It is useful when you manage many locations, when some are managed by franchisees or agencies you do not control, or when you want an audit that does not depend on write access.
What is the Places API (New)?
Places API (New) is Google Maps Platform's current web service for place data. For an audit, two methods matter:
-
Place Details returns one place by ID:
GET https://places.googleapis.com/v1/places/PLACE_ID. -
Text Search finds places from a text query:
POST https://places.googleapis.com/v1/places:searchText.
Both require a field mask. Google's documentation says there is no default list of returned fields, and omitting the mask returns an error. That is a feature. You pay for, and receive, only what you ask for.
How do you get the Place ID for each location?
Two options:
-
From your Business Profile data. The Business Information API's location metadata includes a
placeId. If you have API access, this is the cleanest join. -
From Text Search. Search for "Brand name, street, city" and request
places.id. Google's Text Search page listsplaces.idunder its ID Only SKU, the lowest tier.
curl -X POST "https://places.googleapis.com/v1/places:searchText" \
-H "Content-Type: application/json" \
-H "X-Goog-Api-Key: $PLACES_API_KEY" \
-H "X-Goog-FieldMask: places.id,places.displayName,places.formattedAddress" \
-d '{"textQuery": "Example Hardware, 1200 S Lamar Blvd, Austin, TX"}'
Check the returned name and address by hand before saving the ID. Google's Places API policies say place IDs are exempt from caching restrictions, so you can store them in your location records indefinitely.
Which fields should the audit request?
Only the ones you would act on:
| Field | What it catches |
|---|---|
nationalPhoneNumber |
Wrong or old phone numbers, call tracking numbers you did not intend |
websiteUri |
Homepage links instead of location pages, broken redirects, dropped UTM tags |
businessStatus |
A store showing CLOSED_TEMPORARILY or CLOSED_PERMANENTLY by mistake |
regularOpeningHours |
Hours that differ from your records |
displayName, formattedAddress
|
Name or address drift, and a quick sanity check on the match |
Watch the billing tier. On the Text Search page, Google lists phone numbers, website, and opening hours under a higher SKU than basic fields like address and display name. Place Details uses a similar tiered model. Check current pricing before running this across thousands of locations every day.
How do the hours come back?
regularOpeningHours.periods is a list of open and close points. The Places API reference says day is an integer from 0 to 6 where 0 is Sunday, with separate hour and minute fields. A place open 24 hours can have a period with no close.
There is also currentOpeningHours, which the reference describes as hours for the next seven days, including special hours. Run the audit with that field in the week before a holiday to confirm your holiday exceptions are live.
What does the input file look like?
A CSV exported from your location records:
store_code,place_id,phone,website,status,hours
TX-AUSTIN-014,ChIJ...,+15125550142,https://example.com/locations/texas/austin/south-lamar/,open,mon=08:00-20:00|tue=08:00-20:00|wed=08:00-20:00|thu=08:00-20:00|fri=08:00-20:00|sat=09:00-18:00|sun=closed
What does the audit script look like?
# audit_places.py
# pip install requests
import csv
import os
import re
import sys
from urllib.parse import urlsplit
import requests
API_KEY = os.environ["PLACES_API_KEY"]
FIELDS = ",".join([
"id", "displayName", "formattedAddress", "nationalPhoneNumber",
"websiteUri", "businessStatus", "regularOpeningHours",
])
DAY_INDEX = {"sun": 0, "mon": 1, "tue": 2, "wed": 3, "thu": 4, "fri": 5, "sat": 6}
STATUS = {"open": "OPERATIONAL", "temporarily_closed": "CLOSED_TEMPORARILY",
"permanently_closed": "CLOSED_PERMANENTLY"}
def place_details(place_id: str) -> dict:
resp = requests.get(
f"https://places.googleapis.com/v1/places/{place_id}",
headers={"X-Goog-Api-Key": API_KEY, "X-Goog-FieldMask": FIELDS},
timeout=20,
)
resp.raise_for_status()
return resp.json()
def digits(phone: str) -> str:
d = re.sub(r"\D", "", phone or "")
return d[-10:] # compare US numbers on the last 10 digits
def clean_url(url: str) -> str:
parts = urlsplit(url or "")
return f"{parts.netloc.lower().removeprefix('www.')}{parts.path.rstrip('/')}"
def parse_hours(spec: str) -> set:
# "mon=08:00-20:00|tue=08:00-20:00|sun=closed"
out = set()
for chunk in filter(None, spec.split("|")):
day, value = chunk.split("=")
if value == "closed":
continue
start, end = value.split("-")
out.add((DAY_INDEX[day], start, end))
return out
def google_hours(place: dict) -> set:
out = set()
for p in place.get("regularOpeningHours", {}).get("periods", []):
o, c = p["open"], p.get("close")
if c is None: # open 24 hours
out.add((o["day"], "00:00", "24:00"))
continue
out.add((o["day"], f"{o['hour']:02d}:{o['minute']:02d}", f"{c['hour']:02d}:{c['minute']:02d}"))
return out
def audit_row(row: dict, place: dict) -> list[str]:
issues = []
if digits(place.get("nationalPhoneNumber")) != digits(row["phone"]):
issues.append(f"phone: google={place.get('nationalPhoneNumber')} ours={row['phone']}")
if clean_url(place.get("websiteUri")) != clean_url(row["website"]):
issues.append(f"website: google={place.get('websiteUri')} ours={row['website']}")
expected_status = STATUS.get(row["status"])
if expected_status and place.get("businessStatus") != expected_status:
issues.append(f"status: google={place.get('businessStatus')} ours={row['status']}")
ours, theirs = parse_hours(row["hours"]), google_hours(place)
if ours != theirs:
missing = sorted(ours - theirs)
extra = sorted(theirs - ours)
issues.append(f"hours: missing_on_google={missing} unexpected_on_google={extra}")
return issues
def main(path: str) -> int:
failures = 0
with open(path, newline="") as f:
for row in csv.DictReader(f):
try:
place = place_details(row["place_id"])
except requests.HTTPError as exc:
print(f"{row['store_code']}: lookup failed ({exc.response.status_code})")
failures += 1
continue
issues = audit_row(row, place)
if issues:
failures += 1
print(f"{row['store_code']} ({place.get('displayName', {}).get('text')}):")
for issue in issues:
print(f" - {issue}")
else:
print(f"{row['store_code']}: ok")
return 1 if failures else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1]))
Sample output:
TX-AUSTIN-014 (Example Hardware):
- hours: missing_on_google=[(6, '09:00', '18:00')] unexpected_on_google=[(6, '09:00', '17:00')]
TX-AUSTIN-022: ok
A few deliberate simplifications. Phones compare on the last 10 digits, which suits US numbers. URLs compare host and path and ignore query strings, so UTM tags do not cause false alarms. Overnight hours compare on the opening day only. Tighten any of these for your data.
What should you do with the results?
Treat the output as a work queue, not a dataset. Google's Places API policies say you must not pre-fetch, cache, or store Places content beyond the allowed exceptions, so keep the report short-lived and act on it rather than building a stored copy of Google's data. Review the Google Maps Platform terms for your own use case.
For each flagged location:
- Confirm your own record is right. Sometimes the store changed and nobody told HQ.
- Fix the listing in Google Business Profile, directly or through your listing tool.
- If Google keeps reverting a field, check
getGoogleUpdatedfor that location and accept or reject the update there. - Rerun the audit for that store after the edit is reviewed.
How often should you run it?
Weekly for most brands, daily in the two weeks before major US holidays, and immediately after any bulk change such as a phone system migration or a rebrand.
Run it in CI or a scheduled job and send failures to a channel someone actually watches. An audit nobody reads is just a bill.
Does this replace a listing platform?
No. It checks one platform from the outside. Listing platforms handle publishing to many platforms and usually include their own sync status. Synup, for example, reports per-location sync issues across its publishers. A script like this is a useful second opinion on Google specifically, because it reads the public place record directly instead of trusting any tool's internal status.
FAQ
Do I need Business Profile API access to run this?
No. The Places API uses a standard Google Maps Platform API key. You need Business Profile access only to fix what you find through the API.
Can I store the Places responses for trend reports?
Google's policies restrict caching and storing Places content, with place IDs as the main exception. Store your own pass or fail results and check the terms for anything more.
Why do my hours match in Business Profile but not in the Places API?
Edits can still be in review, or Google may be serving an update from another source. Check getGoogleUpdated for the location.
Does this work for service-area businesses?
Partly. Google's Text Search docs say pure service-area businesses do not include a formattedAddress, so compare phone, website, and hours instead.
Top comments (0)