How We Automated Google Business Profile Data Audits Across Major B2B SaaS Markets
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. Top comments (0)
Comments
No comments yet. Start the discussion.