DEV Community

Cover image for How We Automated Google Business Profile Data Audits Across Major B2B SaaS Markets
MarkoMetrics
MarkoMetrics

Posted on AI-assisted

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
Enter fullscreen mode Exit fullscreen mode

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])
Enter fullscreen mode Exit fullscreen mode

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)