# Google Business Profile Q\&A Scraper (`kayhermes/google-business-profile-q-a-scraper`) Actor

- **URL**: https://apify.com/kayhermes/google-business-profile-q-a-scraper.md
- **Developed by:** [Khoa Nguyen](https://apify.com/kayhermes) (community)
- **Categories:** Social media
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.50 / 1,000 question and answer items

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/platform/actors/running/actors-in-store#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 Q\&A Scraper

Extract public legacy Questions & Answers from specific Google Maps business listings, then normalize, deduplicate, classify, and score the questions for local SEO and owner-response workflows.

This Actor is designed for agencies, local SEO teams, and franchise operators that need structured Q\&A coverage data instead of screenshots or unprocessed page text.

### What it produces

Each dataset item represents one unique question and includes:

- place name, address, place identifier, and source URL
- normalized question and answer text
- author/date/helpful metadata when visible
- owner-answer detection and coverage status
- topic classification across 10 local-business topics
- response quality score, priority score, and risk flags
- ISO timestamps and extractor provenance

The key-value store also receives:

- `OUTPUT`: run summary and coverage counts
- `ERRORS`: per-place failures that are not billed as dataset items
- `DIAGNOSTICS`: page states and extraction warnings
- `REPORT.html`: an owner-response coverage report

### Two-minute quick start

The default input runs an offline demo and needs no credentials or network access:

```json
{
  "sourceMode": "DEMO"
}
```

The demo is fictional and clearly marked as a fixture. Chromium renders the fixture, then the same DOM extractor, normalization, deduplication, classification, scoring, filtering, dataset, and report pipeline used by live runs processes it.

For a live listing:

```json
{
  "sourceMode": "LIVE_URLS",
  "startUrls": [
    { "url": "https://www.google.com/maps/place/REPLACE_WITH_A_SPECIFIC_LISTING" }
  ],
  "maxQuestionsPerPlace": 100,
  "outputFilter": "NO_OWNER_RESPONSE",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": ["RESIDENTIAL"],
    "apifyProxyCountry": "US"
  }
}
```

Use a specific place/listing URL, not a Google search results page.

### Source modes

| Mode | Purpose | Network required |
|---|---|---:|
| `DEMO` | Store QA and first-run demonstration using a realistic offline fixture | No |
| `LIVE_URLS` | Open public Google Maps listing URLs in Chromium | Yes |
| `IMPORTED_HTML` | Reprocess saved rendered listing/Q\&A HTML | No |
| `IMPORTED_JSON` | Normalize and score previously collected Q\&A JSON | No |

Imported data still passes the core normalization, deduplication, topic classification, response scoring, filters, and output schema. Invalid imported records become explicit entries in `ERRORS`; valid records continue processing.

### Main inputs

- `startUrls`: up to 100 Google Maps place URLs
- `maxPlaces`: hard cap on source listings
- `maxQuestionsPerPlace`: per-place output/scroll cap
- `outputFilter`: all, unanswered, no owner response, or owner answered
- `topics`: optional topic allowlist
- `questionTextContains`: normalized case-insensitive substring filter
- `includeAnswerDetails`: remove answer text while retaining coverage metrics
- `maxConcurrency`: bounded number of Chromium sessions
- `maxRequestRetries`: exponential-backoff retries with fresh browser sessions
- `navigationTimeoutSecs` and `requestHandlerTimeoutSecs`: hard timeout controls
- `saveDebugArtifacts`: opt-in rendered HTML and screenshot on live failures

See [`.actor/input_schema.json`](.actor/input_schema.json) for the complete validated schema.

### Output example

```json
{
  "itemType": "question",
  "placeName": "Northstar Coffee & Kitchen",
  "sourceUrl": "https://www.google.com/maps/place/Northstar+Coffee+%26+Kitchen/?hl=en",
  "questionId": "q_...",
  "questionText": "Is the entrance wheelchair accessible?",
  "topic": "accessibility",
  "answerCount": 0,
  "answers": [],
  "hasAnyAnswer": false,
  "hasOwnerAnswer": false,
  "coverageStatus": "unanswered",
  "responseQualityScore": 0,
  "priorityScore": 100,
  "riskFlags": ["UNANSWERED", "RECENT_UNANSWERED"],
  "pageState": "legacy_qa",
  "scrapedAt": "2026-07-25T20:00:00.000Z"
}
```

Complete examples are in [`sample-output`](sample-output).

### Page states

The Actor never treats every empty result as success. Diagnostics distinguish:

- `legacy_qa`: a legacy Q\&A label or questions were found
- `ask_maps_only`: the interface exposes Ask Maps/Ask about this place but no legacy Q\&A
- `no_qa`: no legacy Q\&A section was detected
- `blocked`: bot/unusual-traffic challenge detected
- `consent_required`: consent interstitial remained visible
- `failed`: navigation or extraction failed after retries

### Reliability design

- Chromium is controlled directly through the Chrome DevTools Protocol.
- Live concurrency is bounded; each retry receives a fresh browser profile.
- Retries use exponential backoff and only retry likely transient failures.
- Navigation and total per-place processing have independent hard timeouts.
- Records are deterministically normalized and deduplicated.
- Error records are stored separately from billable question results.
- Logs redact token/password/authorization-like fields and proxy credentials.
- No login, cookie injection, CAPTCHA bypass, or authentication bypass is implemented.

### Proxy guidance

Demo and import modes need no proxy. Live Google Maps runs can be rate-limited or challenged, so low concurrency and an appropriate proxy are recommended. The Actor supports Apify Proxy environment variables and user-provided proxy URLs from the standard proxy input. Proxy credentials are not logged.

### Pricing recommendation

The roadmap launch price is **$2.50 per 1,000 question results** and the target price is **$3.75 per 1,000 question results**. Configure pay-per-event using Apify's synthetic `apify-default-dataset-item` event. One normalized question equals one default dataset item. Errors and reports are written to the key-value store, so they do not create dataset-item events.

The final Store price should be rechecked against measured browser and proxy costs before publication.

### Limitations

- Google can change Maps markup, labels, and feature availability without notice.
- Legacy Q\&A may not be present on every listing or locale.
- Owner identification is based on visible owner/business-owner labels; unlabeled answers cannot be reliably inferred as owner responses.
- Relative dates are normalized when recognized; otherwise the original text is retained and the ISO field is `null`.
- The Actor collects public listing content only. Users remain responsible for applicable terms, privacy obligations, and local law.

### Local development

Requirements: Node.js 22 and Chromium at `/usr/bin/chromium` or `CHROMIUM_PATH`.

```bash
npm ci
npm run typecheck
npm run lint
npm test
npm run validate:schemas
npm run smoke
```

The source package vendors only the TypeScript compiler, Node type declarations, and Undici type declarations as local development dependencies so installation and validation do not depend on the npm registry. Runtime production dependencies are zero.

### Storage and cost drivers

Primary cost drivers are Chromium runtime, listing count, Q\&A panel scroll depth, retries, concurrency, and proxy traffic. Dataset size grows linearly with unique questions. Debug HTML/screenshots are opt-in because they can increase key-value store usage and may include public contributor names visible on the page.

# Actor input Schema

## `sourceMode` (type: `string`):

DEMO runs a realistic offline fixture through the same DOM extraction pipeline. LIVE\_URLS opens Google Maps place URLs. IMPORTED\_HTML processes saved rendered pages. IMPORTED\_JSON normalizes previously collected Q\&A records.

## `startUrls` (type: `array`):

Specific Google Maps business/place URLs. Search-result pages and arbitrary websites are rejected. Short maps.app.goo.gl links are accepted but may require proxy-supported redirects.

## `importedHtmlPages` (type: `array`):

For IMPORTED\_HTML. Each item must contain sourceUrl and renderedHtml captured after opening the Q\&A panel. Useful for audits and regression runs without live navigation.

## `importedPlaces` (type: `array`):

For IMPORTED\_JSON. Each place can include sourceUrl, placeName, address, placeId, and questions\[]. Imported records still pass normalization, deduplication, topic classification, scoring, filtering, and schema checks.

## `maxPlaces` (type: `integer`):

Hard cap on unique place sources processed in one run.

## `maxQuestionsPerPlace` (type: `integer`):

Stops scrolling and output after this many deduplicated questions for each place.

## `outputFilter` (type: `string`):

Return all questions, only unanswered questions, only questions without an owner response, or only questions answered by the owner.

## `topics` (type: `array`):

Optional topic filter. Leave empty to include every classified topic.

## `questionTextContains` (type: `string`):

Optional case-insensitive substring filter applied after normalization.

## `includeAnswerDetails` (type: `boolean`):

Include answer text, author, owner signal, date, and helpful count. When disabled, answer counts and coverage fields remain available.

## `locale` (type: `string`):

BCP-47 style locale used for Accept-Language and hl parameters. Extraction recognizes common English Q\&A labels and semantic DOM signals; non-English interfaces may reduce coverage.

## `maxConcurrency` (type: `integer`):

Google Maps is resource-intensive. Start low; higher values increase block risk and memory use.

## `maxRequestRetries` (type: `integer`):

Retries transient navigation, timeout, and blocked-page failures with exponential backoff and fresh Chromium sessions.

## `navigationTimeoutSecs` (type: `integer`):

Maximum time for initial Google Maps navigation.

## `requestHandlerTimeoutSecs` (type: `integer`):

Maximum total processing time for one listing, including Q\&A panel scrolling.

## `scrollAttempts` (type: `integer`):

Maximum panel scrolls used to load additional questions. Stops early when no new content appears.

## `proxyConfiguration` (type: `object`):

Optional for demo/import modes. Strongly recommended for live Google Maps runs. Residential proxy access may be required depending on volume and region.

## `saveDebugArtifacts` (type: `boolean`):

On live failures, save a screenshot and rendered HTML in the default key-value store. Disabled by default because pages can contain public contributor names and other page content.

## Actor input object example

```json
{
  "sourceMode": "DEMO",
  "startUrls": [],
  "importedHtmlPages": [],
  "importedPlaces": [],
  "maxPlaces": 20,
  "maxQuestionsPerPlace": 100,
  "outputFilter": "ALL",
  "topics": [],
  "questionTextContains": "",
  "includeAnswerDetails": true,
  "locale": "en-US",
  "maxConcurrency": 2,
  "maxRequestRetries": 2,
  "navigationTimeoutSecs": 60,
  "requestHandlerTimeoutSecs": 150,
  "scrollAttempts": 12,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "saveDebugArtifacts": false
}
```

# Actor output Schema

## `questionResults` (type: `string`):

No description

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

No description

## `errors` (type: `string`):

No description

## `report` (type: `string`):

No description

## `diagnostics` (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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("kayhermes/google-business-profile-q-a-scraper").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 = {}

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

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

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

```

## CLI example

```bash
echo '{}' |
apify call kayhermes/google-business-profile-q-a-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=kayhermes/google-business-profile-q-a-scraper",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

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