# Google Business Profile Review Scraper (`usestring/google-business-reviews`) Actor

Find recent low-star Google Business Profile reviews by search term and location, with the business's contact details on every row.

- **URL**: https://apify.com/usestring/google-business-reviews.md
- **Developed by:** [String](https://apify.com/usestring) (community)
- **Categories:** Lead generation, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.42 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Google Business Profile Review Scraper — recent 1-star reviews across a whole area

This Actor finds **recent low-star reviews** on Google Business Profiles across a search term and a
location, and puts the business's contact details on every one of them. Give it `plumber` and
`Austin, TX` and you get back each 1-star review left in the last six months — the **review text, its
date and its author**, alongside the **phone number, website and postal address** of the business it
was left on.

The rating filter and the recency window are both yours to set, so the same Actor answers "every
1-star this month", "everything at 2 stars or below this year", or simply "every review these
profiles have".

No Google account, API key or cookies are used — this reads what a logged-out visitor sees.

### What it returns

One row per **review**. A business with three matching reviews returns three rows, each carrying the
full business record so any single row is enough to act on.

| Field | Type | Notes |
| --- | --- | --- |
| `rating` | number | The review's stars, 1–5 |
| `publishedAt` | string | ISO timestamp — an exact date, not "3 months ago" |
| `relativeDate` | string | Google's own wording, e.g. `"3 days ago"` |
| `text` | string | The review body, with the reviewer's line breaks intact. `null` for a rating-only review |
| `authorName` | string | |
| `authorReviewCount` | number | How many reviews the author has written overall — a credibility signal. `0` is a real value |
| `imageCount` | number | Photos attached to the review |
| `reviewUrl` | string | Link straight to the review |
| `reviewId` | string | Stable, use it to de-duplicate across runs |
| `businessName` | string | The business as Google lists it |
| `businessRating` | number | The profile's overall star average, e.g. `4.7` |
| `phone` | string | Formatted phone number, e.g. `(512) 309-1487` |
| `website` | string | The business's own site, as Google links it |
| `address` | string | Street address; `null` for a service-area business, which Google also shows without one |
| `categories` | string\[] | Google's own labels, e.g. `["Plumber", "Electrician"]` |
| `latitude`, `longitude` | number | Exact coordinates, ready to map |
| `matchedReviewCount` | number | How many matching reviews this business had in this run — group or prioritise on it |
| `placeId` | string | Google's `ChIJ…` Place ID |
| `dataId` | string | The `0x…:0x…` id Google keys the profile's reviews on |
| `mapsUrl` | string | Link straight to the profile |
| `lowestRatedReviewsRead` | number | How much of the profile's low-star history this run saw (see Reliability) |
| `searchTerm`, `sourceUrl`, `collectedAt` | string | Provenance for every row |

### Input

```json
{
  "searches": ["plumber", "hvac"],
  "location": "Austin, TX",
  "maxReviewRating": 1,
  "withinDays": 180,
  "maxItems": 1000
}
```

| Field | Description |
| --- | --- |
| `searches` | Search terms, e.g. `"plumber"`. Up to 50, each searched separately. |
| `location` | City, state, ZIP or country, appended to every term. Leave empty if your terms already carry a location. |
| `places` | Optional. Google Maps place or review URLs to read directly, instead of or as well as a search. |
| `maxReviewRating` | Returns any review at or below this rating. `1` finds 1-star only; `5` returns every review. Default `1`. |
| `withinDays` | How recent a review has to be to be returned. Default `180`, roughly six months. |
| `maxProfilesPerSearch` | How deep to page each search before filtering. Default and maximum `120`. |
| `countryCode`, `language` | The market the search runs in, e.g. `US` / `en`, or `GB` / `en`. |
| `maxItems` | Cap on dataset items. Default 1000. Free plans stop at 250 requests and 250 results — see below. |
| `concurrency` | Requests in flight at once. Default 3, maximum 5. |

Supply either `searches` or `places` — a run with neither is rejected before it starts. A business
whose reviews all miss your filter simply contributes no rows.

### Use cases

- Reputation-management prospecting: a fresh 1-star review is a business in the market for help, and
  the row already has the number to call
- Agency churn signals: watch your own clients' profiles and get the bad review the day it lands
- Competitor monitoring across a category and city, with the complaint text, not just the average
- Complaint-theme analysis across a whole market segment from the review bodies
- Franchise and multi-location oversight, one row per piece of bad feedback

### Input by URL

`places` accepts a Google Maps place or review URL — the long kind containing `!1s0x…:0x…` — or that
`0x…:0x…` id on its own. A `maps.app.goo.gl` short link and a bare `ChIJ…` Place ID do **not** carry
that id, and are reported in `failures` rather than guessed at. A profile supplied this way returns
its reviews and whatever name its URL contained; `phone`, `website`, `address`, `categories` and the
coordinates come from the search surface, so those fields are `null` on a URL-only run.

### Reliability

Both halves read Google's own JSON endpoints rather than scraping rendered markup, so every value is
exact: ratings are numbers, review dates are absolute timestamps, and the reviewer's own line breaks
survive into `text`.

Reviews are read **worst-first**, which is why one request per profile is enough. Sorted that way, a
single page carries the profile's entire low-star history rather than its most recent 50 reviews — on
a busy profile that page held all 16 one-star and all 6 two-star reviews it had ever received, and the
next page was already entirely five-star, while reading newest-first would have needed eight pages to
cover the same six months. `lowestRatedReviewsRead` reports how many that page held; a value of 50
means it filled up, so a profile with more than 50 reviews at or below its floor may have others this
run did not read.

A search pages up to Google's own ceiling of 120 results per term, and consecutive pages overlap, so
the distinct total per term lands nearer 100. Profiles are de-duplicated on `dataId` before any
reviews are fetched, so a profile that appears on two search pages is never fetched twice.

A profile whose reviews cannot be read is recorded in the run's `SUMMARY` under `failures` rather than
silently contributing nothing — an intercepted response and a profile with genuinely no matching
reviews look identical otherwise, and the difference decides whether a list is complete.

There are no retries by design: the String Unblocker owns proxy rotation and anti-bot solver
selection, so a retry loop here would only re-roll the same block.

### Frequently asked questions

**How do I find businesses with recent bad reviews?** Run this Actor with your search terms and
location and leave `maxReviewRating` at `1` and `withinDays` at `180`. Every row is a 1-star review
from the last six months, with the business it was left on attached.

**Can I widen it beyond 1-star?** Yes. `maxReviewRating` returns any review at or below the value you
set, so `2` catches 1- and 2-star, and `5` returns everything the worst-first page held.

**Does it return the review text?** Yes, with the reviewer's line breaks intact. A rating-only review
with no words returns `null` text and keeps its rating, date and author.

**Why do I get several rows for the same business?** Because each row is one review. Group on
`dataId`, or read `matchedReviewCount`, which tells you how many that business had in this run.

**How many businesses does one search cover?** Up to 120, which is Google's own ceiling per search
term; overlapping pages mean the distinct count is usually nearer 100. Use more search terms, or more
specific ones, to cover a market more deeply.

**Do I need a Google account, API key or cookies?** No. It reads public Google Business Profiles as a
logged-out visitor and never signs in.

**Can I monitor specific businesses instead of searching?** Yes — put their Google Maps URLs in
`places`. See "Input by URL" for which URL forms carry the id this needs.

**Can I search several cities in one run?** `location` is one value applied to every term, so run the
Actor once per city — or put each city into the search terms themselves and leave `location` empty.

### Limitations

Search and review data only: no opening hours, no menus, no photos, no popular-times data and no
"about" text. Reviews are read one worst-first page deep, so this is not a full review-history export
for a profile with more than 50 low-rated reviews. Profiles supplied by URL in `places` carry no
address, phone, website or coordinates, because those come from the search surface rather than the
review endpoint. Google orders a search by its own relevance rather than anything stable, so
re-running can surface a business at a different rank; de-duplicate on `reviewId`.

### Free plan limit

Runs started from an Apify **free plan** stop at **250 requests and 250 results**, and the run reports
that it reached the limit. Any paid plan runs the full input and `maxItems` you set.

The limit exists because this Actor fetches through our own infrastructure, which Apify does not cover
for free-plan runs. It binds on requests as well as results, and one ceiling covers both the search
and the review stage together, so a large input cannot spend those fetches for rows the run will not
return.

# Actor input Schema

## `searches` (type: `array`):

What to look for, for example plumber. Each term is searched on its own.

## `location` (type: `string`):

City, state, ZIP or country to search in. Appended to each search term, so leave it empty if the terms already carry a location.

## `places` (type: `array`):

Google Maps place or review URLs to read instead of, or as well as, a search. A maps.app.goo.gl short link does not carry the id these need.

## `maxReviewRating` (type: `integer`):

A profile matches on any review at or below this rating. 1 finds 1-star only.

## `withinDays` (type: `integer`):

How recent a low-star review has to be to count. 180 is roughly six months.

## `maxProfilesPerSearch` (type: `integer`):

How deep to page each search before filtering. Google serves at most 120, and consecutive pages overlap.

## `countryCode` (type: `string`):

ISO country code the search is run as, for example US or GB.

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

Language code for the results, for example en.

## `maxItems` (type: `integer`):

Global cap on dataset items. Runs started from an Apify free plan stop at 250 requests and 250 results; any paid plan runs the full amount.

## `concurrency` (type: `integer`):

Targets fetched in parallel.

## Actor input object example

```json
{
  "searches": [
    "plumber"
  ],
  "location": "Austin, TX",
  "maxReviewRating": 1,
  "withinDays": 180,
  "maxProfilesPerSearch": 120,
  "countryCode": "US",
  "language": "en",
  "maxItems": 1000,
  "concurrency": 3
}
```

# Actor output Schema

## `results` (type: `string`):

Find recent low-star Google Business Profile reviews by search term and location, with the business's contact details on every row.

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

Item count, failure count and every target that failed, with its error.

# 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 = {
    "searches": [
        "plumber"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("usestring/google-business-reviews").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 = { "searches": ["plumber"] }

# Run the Actor and wait for it to finish
run = client.actor("usestring/google-business-reviews").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 '{
  "searches": [
    "plumber"
  ]
}' |
apify call usestring/google-business-reviews --silent --output-dataset

```

## MCP server setup

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

```

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/UDDAOv1Bo3s3iMdA8/builds/E0K7BeV1qUeOT5uJC/openapi.json
