# Google Business Profile Audit & Competitor Check (`steadyscrape/google-business-profile-audit`) Actor

Google Business Profile (GBP, GMB) audit tool for local SEO: a 0–100 score, what is missing and why it matters, owner replies, posts, photos, hours, booking links, and a comparison with the competitors Google Maps ranks around it. By link, place id, CID or name. Report as CSV, JSON or API. No login.

- **URL**: https://apify.com/steadyscrape/google-business-profile-audit.md
- **Developed by:** [Steady Scrape](https://apify.com/steadyscrape) (community)
- **Categories:** SEO tools, Marketing, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$20.00 / 1,000 business auditeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Google Business Profile Audit & Competitor Check (GMB audit)

Audit any **Google Business Profile** (formerly Google My Business) in seconds: a **0–100 completeness score**, a
**letter grade**, the **list of what is missing — each item with why it matters and how to fix it** — and a
**comparison with the competitors Google Maps ranks first around the business**: their scores, ratings, reviews and
photos, and **which items they have that the business lacks**.

One business or 500 in a run. Give a Google Maps link, a place id, a CID, or just **"name, city"**. No login, no
browser, no Google API key, no other Actor to run first.

### What it reads from each profile

Everything Google Maps shows a signed-out visitor on the profile:

| Area | Fields |
|---|---|
| Ownership | `claimed` (false when Google offers "Claim this business"), `markedClosed` |
| Contact | `website` and `websiteType` (`own`, `free-builder`, `social`, `directory`, `none` — a Facebook page is not a website), `phone`, `phoneInternational`, `address`, `lat`, `lng` |
| Categories | `primaryCategory`, `additionalCategories` |
| Hours | `openingHours` (one line per day), `moreHours` (Happy hours, Takeout, Delivery…) |
| Content | `description` (the owner's "From the business" text) and `descriptionLength`, `googleSummary` (the line Google writes itself), `photoCount` |
| Activity | `postsShown`, `latestPostAt` (owner updates), `newestReviews`: the five newest reviews' dates, average stars, **how many have an owner reply** and the date of the latest reply |
| Reputation | `rating`, `reviewCount`, `ratingHistogram` (1★ to 5★ counts), `reviewTopics` (what reviews mention most, with counts) |
| Conversion | `actionLinks` (Book online / Reserve a table / Order online, with the booking partner when Google names it), `reservationLinks` |
| Extras | `accessibility` (the features the place has), `highlights` (owner badges such as "Identifies as women-owned") |

No reviewer names or review texts are written: the audit only counts and dates them.

### The score and its weights

Thirteen items, 100 points. Every check in the output says its points, its maximum, the value found, why it matters,
and the fix when it is not complete.

| Item | Points | Full marks when |
|---|---|---|
| Profile claimed by its owner | 10 | no "Claim this business" on the profile |
| Own website linked | 12 | a site on the business's own domain (free builder 70 %, social or directory page 30 %) |
| Phone number | 8 | a phone is listed |
| Opening hours | 8 | weekly hours are set |
| Business description | 8 | 250+ characters (shorter: half) |
| Primary and additional categories | 6 | primary + 2 or more additional (1 additional: 60 %, primary only: 30 %) |
| Photos | 10 | 100+ (25+: 70 %, 10+: 40 %, 1+: 10 %) |
| Star rating | 10 | 4.5+ (4.0+: 70 %, 3.5+: 40 %) |
| Number of reviews | 10 | 100+ (50+: 70 %, 20+: 40 %, 1+: 10 %) |
| Recent reviews | 5 | newest review within 30 days (within 90: half) |
| Owner replies to reviews | 7 | the owner answered all five newest reviews (pro rata) |
| Recent updates (posts) | 4 | a post within 30 days (within 90: half) |
| Booking, reservation or order link | 2 | any Book, Reserve or Order button or reservation link |

Grades: **A** 85+, **B** 70+, **C** 55+, **D** 40+, **F** below 40.

### The competitor comparison

For each business, the Actor runs **one Google Maps search for its primary category** (or a keyword you choose, such
as `emergency dentist`) at the business's own location, the way a customer nearby sees it, and audits the first
**N** places (default 3, up to 10) with the same score. You get:

- `competitorKeywordRank`: **where the business itself ranks** for that search at its own door (1–20, or `null`);
- `competitors`: each one's rank, score, grade, rating, reviews, photos, claimed status, website type, newest review,
  latest post, owner replies, and the items it has in full;
- `gaps`: the items on which competitors score more than the business, with how many and which;
- `benchmarks`: the competitors' median score, rating, reviews and photos, and the business's position among them.

Other branches of the same brand (places linking the same website) are never counted as competitors. Businesses of
one city often share competitors: each place is read once per run.

### Built for local SEO agencies

- **Prospecting**: audit a list of businesses of one niche and city and pitch the D and F profiles first — unclaimed
  ones, no website, no replies to reviews — with a ready "what to fix" list for each.
- **Client onboarding and monthly reports**: the score, the missing list and the gap to the top 3 competitors, in a
  dataset you can push to a sheet, a CRM or a PDF template.
- **Before / after**: re-run the same audit after the profile work and show the score move.
- **Companion to rankings**: pair it with our
  [Google Maps Rank Tracker (geo-grid)](https://apify.com/steadyscrape/google-maps-rank-tracker) to show where the
  business ranks across town and what to change on the profile to rank higher.

### Input example

```json
{
    "businesses": [
        "https://www.google.com/maps/place/Swish+Dental+Downtown/data=!4m2!3m1!1s0x8644b5b434046875:0x644e6eb422388b7a",
        "ChIJM8w7rqS1RIYRhEz2-KuoqzE",
        "Uchi Austin, Austin TX"
    ],
    "competitors": 3,
    "competitorKeyword": "",
    "language": "en",
    "country": "us"
}
```

A link, place id or CID is exact. A name is looked up on Google Maps and accepted only when Google answers with a
single place or a place that carries that name; otherwise the row says which places Google listed and asks for the
link.

### Output example (shortened)

```json
{
    "rowType": "audit",
    "status": "ok",
    "name": "Swish Dental Downtown",
    "placeId": "ChIJdWgENLS1RIYReos4IrRuTmQ",
    "cid": "7227836171923524474",
    "mapsUrl": "https://www.google.com/maps?cid=7227836171923524474",
    "score": 94,
    "grade": "A",
    "missing": [
        { "item": "photos", "label": "Photos", "pointsLost": 3, "value": "25 photo(s)", "why": "Profiles with many photos get more direction requests and website clicks; recent photos also show the place is real and active.", "fix": "Upload photos of the outside, inside, team and work until the profile has 100 or more." },
        { "item": "ownerReplies", "label": "Owner replies to reviews", "pointsLost": 3, "value": "owner replied to 3 of the 5 newest reviews", "why": "Answering reviews is a trust and ranking signal; customers read the replies, above all to bad reviews.", "fix": "Reply to every review, the newest first, within a few days." }
    ],
    "claimed": true,
    "primaryCategory": "Dentist",
    "additionalCategories": ["Cosmetic dentist", "Dental clinic", "…"],
    "websiteType": "own",
    "phone": "(512) 713-1099",
    "rating": 4.5,
    "reviewCount": 991,
    "ratingHistogram": { "oneStar": 81, "twoStars": 23, "threeStars": 18, "fourStars": 31, "fiveStars": 838 },
    "photoCount": 25,
    "descriptionLength": 610,
    "postsShown": 5,
    "latestPostAt": "2026-09-29T22:13:17.000Z",
    "newestReviews": { "sampleSize": 5, "newestAt": "2026-10-16T01:21:40.118Z", "oldestAt": "2026-10-13T22:37:00.939Z", "averageStars": 3.8, "withOwnerReply": 3, "latestOwnerReplyAt": "2026-10-16T03:22:12.000Z" },
    "actionLinks": [{ "kind": "book", "label": "Book online", "provider": "subscribili.com" }],
    "competitorComparison": "done",
    "competitorKeyword": "Dentist",
    "competitorKeywordRank": 3,
    "competitors": [
        { "rank": 1, "name": "Breeze Dental", "score": 92, "grade": "A", "rating": 4.8, "reviewCount": 1002, "photoCount": 26, "claimed": true, "ownerRepliesInNewest": "3 of 5", "…": "…" }
    ],
    "gaps": [{ "item": "ownerReplies", "label": "Owner replies to reviews", "competitorsAhead": 1, "competitorsCompared": 3, "competitors": ["Toothbar"] }],
    "benchmarks": { "competitorsCompared": 3, "medianScore": 92, "medianRating": 4.9, "medianReviewCount": 735, "medianPhotoCount": 47, "reviewCountPosition": 2, "photoCountPosition": 4, "scorePosition": 1 },
    "checks": ["… all thirteen checks with points, value, why and fix …"]
}
```

An input that could not be audited is a free row with `rowType: "business"` and a `status`: `business_not_found`,
`invalid`, `blocked` or `spending_limit`, with the reason.

### What is not available without logging in (and is not promised)

Google Maps shows a signed-out visitor less than the owner sees in their dashboard. This Actor does **not** report:
social profile links, the menu link, the services and products lists, the full "About" attribute list beyond
accessibility features and highlights, owner photos apart from customer photos (`photoCount` is the profile's total),
the reply rate over all reviews (only the five newest are read), Q\&A, or the owner's insights (calls, clicks,
searches). Items that need a login are left out of the score rather than guessed.

### Pricing

Pay per event, no start fee:

| Event | Price |
|---|---|
| `audit` — one business audited, **competitor comparison included** | **US$0.02** |

100 audits with 3 competitors each: **US$2**. Free: businesses not found, invalid inputs, blocked rows, businesses
left out by your spending limit, and an audit whose competitor comparison could not finish because Google refused
it. The spending limit is checked before each audit starts, so nothing is read that your limit cannot pay for.

### Stops when Google asks for verification

The Actor reads the same answers Google Maps gives its own web page, at a moderate pace (two requests in flight).
**If Google ever asks for verification** — its "unusual traffic" page, a CAPTCHA, a JavaScript check, or any answer
that is not Google Maps' normal data — **the run stops sending requests**: every business left is a free `blocked`
row that says so, and the run ends normally with a message. It never solves, bypasses or replays a challenge. Plain
overloads (HTTP 429 or 5xx without a challenge) are retried through Apify's datacenter proxy.

### Limits

- Up to 500 businesses and 10 competitors each per run.
- The newest-reviews figures cover the five newest reviews Google shows (ordered by last edit, as Google orders them).
- Ranks and competitors are those of Google Maps' list for the search at the business's location, seen as a signed-out
  visitor from the run's IP; they can differ slightly from what a logged-in user sees.
- Language and country set how categories, hours and labels are written; use the business's own country.

### Is it legal?

It reads publicly visible business profiles, at a pace a person browsing the map could produce, without logging in.
It writes no personal data of reviewers. Use the data in line with Google's terms and the laws that apply to you.

# Actor input Schema

## `businesses` (type: `array`):

One per line: the business's <b>Google Maps link</b> (the place page, or a <code>maps.app.goo.gl</code> share link), its <b>place id</b> (<code>ChIJ…</code>), its <b>CID</b>, or its <b>name and city</b> (<code>Swish Dental Downtown, Austin TX</code>). A link or an id is exact; a name is looked up on Google Maps and only accepted when the place found carries that name. Up to 500.

## `competitors` (type: `integer`):

How many of the places Google Maps ranks first for the business's primary category around it are compared with it (their profiles are audited the same way). 0 skips the comparison. Other branches of the same brand (same website) are left out. Included in the price of each audit.

## `competitorKeyword` (type: `string`):

What competitors are searched for around each business, e.g. <code>emergency dentist</code>. Empty: each business's own primary category on Google (Dentist, Plumber, Sushi restaurant…). The business's own rank for this search is reported too.

## `language` (type: `string`):

Language of categories, hours and labels, as a code: <code>en</code>, <code>es</code>, <code>de</code>, <code>pt-BR</code>.

## `country` (type: `string`):

Two-letter country code Google answers for (<code>us</code>, <code>gb</code>, <code>au</code>). Use the business's country.

## Actor input object example

```json
{
  "businesses": [
    "https://www.google.com/maps/place/Swish+Dental+Downtown/data=!4m2!3m1!1s0x8644b5b434046875:0x644e6eb422388b7a"
  ],
  "competitors": 3,
  "competitorKeyword": "",
  "language": "en",
  "country": "us"
}
```

# Actor output Schema

## `audits` (type: `string`):

No description

## `summary` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "businesses": [
        "https://www.google.com/maps/place/Swish+Dental+Downtown/data=!4m2!3m1!1s0x8644b5b434046875:0x644e6eb422388b7a"
    ],
    "competitors": 3
};

// Run the Actor and wait for it to finish
const run = await client.actor("steadyscrape/google-business-profile-audit").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "businesses": ["https://www.google.com/maps/place/Swish+Dental+Downtown/data=!4m2!3m1!1s0x8644b5b434046875:0x644e6eb422388b7a"],
    "competitors": 3,
}

# Run the Actor and wait for it to finish
run = client.actor("steadyscrape/google-business-profile-audit").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "businesses": [
    "https://www.google.com/maps/place/Swish+Dental+Downtown/data=!4m2!3m1!1s0x8644b5b434046875:0x644e6eb422388b7a"
  ],
  "competitors": 3
}' |
apify call steadyscrape/google-business-profile-audit --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,steadyscrape/google-business-profile-audit"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/cQsUlhdJ4JWHNU1pd/builds/jNThhFRqUb7N0RzV6/openapi.json
