Backend
How We Automated Google Business Profile Data Audits Across Major B2B SaaS Markets
MarkoMetrics Dev.to (EN Zone)
4 views
If you manage local or multi-location SEO for a B2B SaaS company operating across several markets, you already know the pain: Google Business Profiles (GBP) drift out of sync fast. Categories get changed by well-meaning support staff, NAP (name/address/phone) data goes stale after office moves, hours don't get updated for holidays in different regions, and nobody notices until rankings quietly slip.
At MarkoMetrics we manage GBP listings across multiple markets (UK, Singapore, Malaysia, Indonesia, Philippines) for SaaS clients expanding into Southeast Asia. Manually checking each profile every week doesn't scale. So we built a lightweight audit script using the Google Business Profile API to flag inconsistencies automatically.
Here's how it works, and the code so you can adapt it for your own stack.
The problem with manual GBP audits
Data drift: fields get edited by multiple team members with no single source of truth
Category mismatches: businesses often end up with an overly generic primary category, which hurts relevance
NAP inconsistency: address formatting differs across markets (postal code placement, unit numbering conventions)
Silent errors: a wrong phone number or closed status can sit unnoticed for weeks
None of these are catastrophic individually, but together they erode local ranking signals and trust — and B2B SaaS companies with multiple regional entities (or one HQ profile per country) accumulate this drift fast.
What the script checks
We built a Python script that pulls each profile via the API and flags:
Primary/secondary category mismatches against a defined "expected categories" list
Missing or malformed phone numbers per market (using phonenumbers for country-specific validation)
Address field completeness and consistency against a canonical CRM record
Business hours gaps or overlaps
Whether the profile is verified and not suspended
Setup
You'll need:
A Google Cloud project with the Business Profile API enabled
OAuth2 credentials for an account with access to your Business Profile locations
google-api-python-client, phonenumbers, and pandas
pip install google-api-python-client google-auth-oauthlib phonenumbers pandas
The audit script
import pandas as pd
import phonenumbers
from googleapiclient.discovery import build
from google.oauth2.credentials import Credentials
# Canonical data — your source of truth (e.g. pulled from a CRM export)
CANONICAL = {
"location_id_1": {
"expected_category": "Software Company",
"country_code": "SG",
"expected_phone": "+65 1234 5678",
},
# ... add per-location records
}
def get_service(creds_path):
creds = Credentials.from_authorized_user_file(creds_path)
return build("mybusinessbusinessinformation", "v1", credentials=creds)
def audit_location(service, account_id, location_id, canonical):
location = service.accounts().locations().get(
name=f"accounts/{account_id}/locations/{location_id}",
readMask="categories,phoneNumbers,storefrontAddress,regularHours,openInfo"
).execute()
issues = []
# Category check
primary_category = location.get("categories", {}).get("primaryCategory", {}).get("displayName")
if primary_category != canonical["expected_category"]:
issues.append(f"Category mismatch: got '{primary_category}', expected '{canonical['expected_category']}'")
# Phone validation
phone = location.get("phoneNumbers", {}).get("primaryPhone")
if phone:
try:
parsed = phonenumbers.parse(phone, canonical["country_code"])
if not phonenumbers.is_valid_number(parsed):
issues.append(f"Invalid phone format: {phone}")
except phonenumbers.NumberParseException:
issues.append(f"Unparseable phone number: {phone}")
else:
issues.append("Missing phone number")
# Verification / open status
open_info = location.get("openInfo", {})
if open_info.get("status") != "OPEN":
issues.append(f"Location status is '{open_info.get('status')}', not OPEN")
# Hours check — flag empty hours as a gap
if not location.get("regularHours"):
issues.append("No business hours set")
return {
"location_id": location_id,
"issue_count": len(issues),
"issues": "; ".join(issues) if issues else "OK",
}
def run_audit(service, account_id, canonical_map):
results = [
audit_location(service, account_id, loc_id, data)
for loc_id, data in canonical_map.items()
]
return pd.DataFrame(results)
if __name__ == "__main__":
service = get_service("credentials.json")
df = run_audit(service, account_id="YOUR_ACCOUNT_ID", canonical_map=CANONICAL)
df.to_csv("gbp_audit_results.csv", index=False)
print(df[df["issue_count"] > 0])
Run this weekly (cron, GitHub Actions, whatever fits your stack) and pipe the CSV into Slack or email so the team sees drift before it becomes a ranking problem.
Results from running this across our client base
Since automating this, we catch category and phone-format issues within a week of them occurring instead of during a quarterly manual review — which, across a multi-market SaaS footprint, used to mean weeks of degraded local visibility going unnoticed in at least one market at any given time.
Extending it further
A few directions worth taking this:
Add a diff-tracking layer (store daily snapshots, alert only on changes rather than re-flagging static issues)
Extend CANONICAL to pull dynamically from a CRM API instead of a hardcoded dict
Add review-response latency tracking using the Reviews API — slow review responses are also a local ranking factor
If you're running local SEO for a multi-market company and want to compare notes on GBP automation, happy to chat in the comments. We write more about local SEO/GBP work over at the MarkoMetrics blog.
Read original: https://dev.to/markometrics/how-we-automated-google-business-profile-data-audits-across-major-b2b-saas-markets-280d
← Previous
I built a free in-browser EU CRA Article 14 checker — paste a lockfile, nothing uploaded
Next →
Five Browser Games, Zero Dependencies: What Building a Game Center in Vanilla JS Actually Taught Me
Related
GraphQL @oneOf: Exactly One Input, Enforced by the Schema
Backend
2
DEV Community
The Zero-Code Guide to Python: Functions, Built-in Errors & Clean File Handling
Backend
1
DEV Community
The Deployment Failure That Only Shows Up on a Clean Clone
Backend
1
DEV Community
Decoupled Security: Securing the Auth Flow Without Touching the Implementation
Backend
2
DEV Community
Comments0
No comments yet — be the first