Frontend
Checking If a Business's Google Profile Actually Matches Its Own Website
orange DEV Community
1 views
If you do local SEO work, you've run into this: a client's Google Business Profile says one phone number, their website footer says another, and nobody noticed until a customer called the wrong number. Or the postal code on the GBP listing is a leftover from an old office. This kind of drift is called a NAP (Name, Address, Phone) inconsistency, and it's widely cited as a local search ranking factor. But checking it by hand means opening every listing and every website side by side.
I built Google Maps NAP Consistency Checker, an Apify Actor that takes Google Maps scraper output, fetches each business's own website (lightly: homepage plus one likely subpage), and checks whether the name, postal code, and phone number on the Google Business Profile actually show up on the site.
What it does, and what it doesn't
This Actor checks one thing: does a business's own website agree with its Google Business Profile on name, postal code, and phone number. It does not check third-party directories (Yelp, Facebook, etc.). That's a different problem with a different competitor landscape. It does not crawl an entire website; it fetches at most two pages per business (homepage, plus a subpage if one with a keyword like "contact" or "about" is linked from it). It does not use an LLM. It's regex and string matching against fetched text, which makes it fast, cheap, and predictable. There's no model that can hallucinate a match that isn't there.
Businesses with no independent website (only a social profile, or nothing) are skipped entirely, because there's nothing to fetch and compare against.
How it works
For each place with a real website, the Actor:
Checks robots.txt for that domain before fetching anything, and skips the business if the checker's user agent isn't allowed.
Fetches the homepage HTML (up to 3 MB), strips <script>, <style>, and comments before converting to text, so JavaScript variables and tracking IDs don't get misread as phone numbers.
Looks for an internal link containing a keyword like "contact," "about," "access," or "info" (also handles Japanese equivalents), and fetches that one extra page if found. Mail and tel links and JS pseudo-links are excluded from that search. An early version tried to fetch mailto:info@... as if it were a page URL, which is exactly the kind of bug you only find on real sites.
Pulls out href="tel:..." numbers directly (a strong signal), plus any phone-shaped strings in the visible text, covering Japanese formats (03-6434-9090) and US formats ((512) 828-3835, +1 512-828-3835).
Compares postal code and phone number found on the site against what the Google Business Profile has, using the scraper's own postalCode field when available, with digit-only and hyphenated variants both checked so formatting differences don't cause false mismatches.
Checks whether a meaningful fraction of the business name's word tokens appear anywhere on the page, as a rough "is this even the right site" sanity check.
Every result also gets a changeStatus: run it again later on the same list and matches that were previously flagged as mismatched but are now fine get tagged FIXED. Useful if you're the one who told the client to fix it and want proof it happened.
Usage
Apify Console
Run a Google Maps scraper (e.g. Google Maps Scraper) for the businesses you want to audit.
Open Google Maps NAP Consistency Checker, paste that dataset array into the Google Maps places field.
Click Start. You're billed per business actually checked. Places with no independent website are skipped for free.
Chaining two Actors with apify-client (Python)
from apify_client import ApifyClient
client = ApifyClient(token="YOUR_APIFY_TOKEN")
# 1. Scrape a set of businesses
scrape_run = client.actor("compass/crawler-google-places").call(
run_input={
"searchStringsArray": ["plumber in Austin, TX"],
"maxCrawledPlacesPerSearch": 100,
}
)
places = list(client.dataset(scrape_run["defaultDatasetId"]).iterate_items())
# 2. Check NAP consistency for each business's own website
nap_run = client.actor("reverenced_garnet/apify-gmaps-nap-checker").call(
run_input={"places": places}
)
results = list(client.dataset(nap_run["defaultDatasetId"]).iterate_items())
mismatches = [r for r in results if r["reasons"]]
print(f"{len(mismatches)} of {len(results)} checked businesses have a NAP issue")
Input
Field
Type
Description
places
array (required)
Place objects from a Google Maps scraper's dataset output. Only places with their own (non-social-media) website are checked. The rest are skipped.
Output
{
"title": "Ultimate Plumber",
"placeId": "ChIJv79rmEfzGGARhXlSEd8ceiQ",
"fetchedOk": true,
"postalMatch": true,
"phoneStatus": "match",
"nameFoundOnSite": true,
"changeStatus": "FIXED",
"reasons": []
}
Field
Type
Description
title
string
Business name
placeId
string
Google Maps place ID
fetchedOk
boolean
Whether the website could be fetched at all (false means every other field is unknown, not "matching")
postalMatch
boolean or null
Whether the GBP postal code appears on the site; null if there wasn't enough data on either side to compare
phoneStatus
string
match, mismatch, gbp_missing_on_site_found, site_missing_gbp_has, both_missing, or unknown
nameFoundOnSite
boolean or null
Whether most of the business name's word tokens appear on the page
changeStatus
string
NEW, UPDATED, UNCHANGED, or FIXED versus the previous run on this Actor
reasons
array
Plain-English list of what didn't match
Note that fetchedOk: false and postalMatch: null are deliberately kept separate from a plain "no mismatch found." If the site couldn't be fetched, or there wasn't a postal code to compare on one side, the Actor says so instead of quietly reporting a false match.
Use case: auditing NAP consistency before a local SEO engagement
A common first step for a local SEO agency taking on a new client, or a freelancer building a pitch:
Scrape the client's own listing (or every location, for a multi-location business) with a Google Maps scraper.
Run it through this Actor to see whether the phone number and address on the GBP listing actually match the website.
Use phoneStatus: gbp_missing_on_site_found as a quick, low-effort recommendation: "your GBP is missing a phone number your own site already lists, this is a five-minute fix."
Schedule it monthly with Apify's Scheduler for an existing client, and use changeStatus: FIXED as evidence the fixes you recommended actually got made.
The same pattern applies to a directory or citation-management tool that wants a quick pre-check on whether a business's own site agrees with itself before flagging bigger discrepancies elsewhere.
A note on scope and responsible use
This Actor fetches at most two pages per business, respects robots.txt for the domain being checked, and identifies itself with its own user agent string. It's not a general-purpose crawler. It only looks at data that's already public on the business's own Google Business Profile and website. It's still worth being explicit that finding a phone number or address on a public page doesn't by itself give you consent to use it for outreach; if you use anything found here for contacting a business, you're responsible for complying with applicable law in your jurisdiction.
Pricing
Pay-per-event: $0.01 when a run starts, plus $0.01 per business actually checked (only businesses that had a website to fetch). Auditing 100 locations costs about $1.01. Places with no independent website are skipped and not charged. Current pricing is always on the Store page.
Try it
The Actor is live on the Apify Store: Google Maps NAP Consistency Checker. I built this as part of a small set of post-processing Actors for Google Maps scraper output. If you're already running a scraper for local SEO or lead work, this is a cheap add-on step to catch a class of client-facing errors that's otherwise easy to miss.
Read original: https://dev.to/orange_k/checking-if-a-businesss-google-profile-actually-matches-its-own-website-413o
← Previous
Trump signs order to remove endangered species protection for grey wolves
Next →
DDoS Protection & Rate Limiting Abuse Cases
Related
H
How to Track Stripe API Changes Automatically (Before They Break Your Code)
Frontend
0
Dev.to (EN Zone)
W
What I Wish I Knew Before Taking My First Freelance Client
Frontend
0
DEV Community
D
"Diagrams in Confluence: draw.io, Mermaid, PlantUML or an attached SVG"
Frontend
1
Dev.to (EN Zone)
P
Posting from a shed with one bar of signal: an offline write queue in plain JS
Frontend
1
Dev.to (EN Zone)
Comments0
No comments yet — be the first