# ReviewForge — Google Maps Reviews & Reputation Monitor (`actorify/reviewforge`) Actor

Monitoring-first Google Maps reviews actor: tells you what changed since last run — new reviews, rating drops, negative reviews, and unanswered reviews — instead of a one-shot bulk dump. Cross-run state + alerts. PAY\_PER\_EVENT.

- **URL**: https://apify.com/actorify/reviewforge.md
- **Developed by:** [fanio zilla](https://apify.com/actorify) (community)
- **Categories:** Automation, Lead generation, Integrations
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.01 / 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.

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.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js/docs.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python/docs.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/integrations/mcp.md).

If your project is in a different language, use 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

## ReviewForge — Google Maps Reviews & Reputation Monitor

ReviewForge is a **monitoring-first** Google Maps reviews actor. Instead of a one-shot bulk dump, it runs on a schedule and tells you **what changed since your last run**: new reviews, negative reviews, unanswered reviews, and rating drops — delivered as delta-tagged records, place snapshots, and an alerts feed.

> Bulk extractors re-scrape everything every run and charge per review fetched. ReviewForge charges per review **new since last run** + per alert, so steady-state monitoring cost tracks *monitoring value*, not fetch volume. A weekly run on 10 places (~2 new reviews each) costs a fraction of re-fetching ~1,500 reviews the way a bulk scraper does.

### What ReviewForge tracks

- New reviews posted since the last run (delta-tagged against your previous run)
- Negative reviews and sentiment shifts (English + Indonesian)
- Rating drops and changes (rating deltas)
- Unanswered reviews
- Place rating, review count, and rating delta over time (place snapshots)

### Why monitor Google Maps reviews?

Google Maps has over 8 billion monthly active users and is one of the most influential platforms for local business reputation and discovery. Your reviews directly impact customer decisions, search rankings, and overall brand perception.

Here are just some of the ways monitoring Google Maps reviews could help your business or organization:

- Quickly respond to negative feedback before it damages your reputation
- Identify trending complaints or issues customers mention repeatedly
- Track competitor review activity and positioning
- Monitor rating trends to assess business health
- Automate alerts for urgent reputation management situations
- Build historical records of your online reputation
- Make data-driven decisions about service improvements

If you would like more inspiration on how scraping Google Maps data could help your business or organization, check out our [industry pages](https://apify.com/industries).

### How to monitor Google Maps reviews with ReviewForge

It's easy to monitor [Google Maps](https://maps.google.com) with ReviewForge. Just follow these steps:

1. Click on **Try for free**.
2. Enter the Google Maps URL(s) for your business location(s) or provide search keywords to identify your business.
3. Configure your notification preferences and monitoring frequency.
4. Click on **Run**.
5. When ReviewForge finishes, review the change report in the Dataset tab showing new reviews, rating changes, and unanswered reviews.

The actor maintains state across runs in a named KeyValueStore (`reviewforge-state`), so it automatically detects what's new and different since your last execution. A failed target is skipped and its watermark is **not** advanced, so a retry re-sees its reviews; the run continues for the other targets.

### How it works (per run)

1. Validate input → resolve `startUrls` / `searchQueries` to google_maps targets.
2. Load `LAST_RUN_STATE` from the named KeyValueStore (`reviewforge-state`). *(The default KV store is purged every run, so state must live in a named store to persist across runs.)*
3. For each target, dispatch through the **source registry** to the `google_maps` fetcher (Playwright + residential proxy): place snapshot + newest reviews.
4. **Delta-tag** each review (`isNewSinceLastRun = reviewId ∉ seenSet`); first run on a place ⇒ all reviews are "new".
5. Score sentiment (bilingual EN+ID keyword lexicon), compute place deltas, emit **alerts** (new / negative / rating-drop / unanswered).
6. Write REVIEWS (default dataset), PLACE-SNAPSHOTS, ALERTS.
7. **On success only**, atomically persist the new `LAST_RUN_STATE`.

### Output — three datasets

- **`REVIEWS`** (default) — one row per fetched review, delta-tagged, with `platform`, `sourceId`, `reviewId`, `rating`, `sentiment`, `tags`, and `isNewSinceLastRun`.
- **`PLACE-SNAPSHOTS`** (named) — per place: `rating`, `reviewCount`, `ratingDelta`, `newReviewCount`.
- **`ALERTS`** (named) — the differentiator: `new_review`, `negative_review`, `rating_drop`, `unanswered` with severity. POSTed to `notifications.webhookUrl` if set.

### Results

ReviewForge returns structured data about changes since the last run:

```json
{
  "location": {
    "name": "Business Name",
    "address": "123 Main St, City, State",
    "placeId": "ChIJ1234567890",
    "url": "https://www.google.com/maps/place/..."
  },
  "currentRating": 4.5,
  "previousRating": 4.6,
  "ratingChange": -0.1,
  "totalReviews": 248,
  "newReviewsCount": 3,
  "newReviews": [
    {
      "author": "John D.",
      "rating": 5,
      "text": "Excellent service, highly recommend!",
      "date": "2024-01-15",
      "isNew": true
    }
  ],
  "negativeReviewsCount": 1,
  "unansweredReviewsCount": 2,
  "unansweredReviews": [
    {
      "author": "Jane S.",
      "rating": 3,
      "text": "Good but could be better",
      "date": "2024-01-10",
      "answered": false
    }
  ],
  "lastRun": "2024-01-15T10:30:00Z"
}
````

### Pricing — PAY\_PER\_EVENT

ReviewForge uses Apify's PAY\_PER\_EVENT pricing model, meaning you only pay for actual changes detected (new reviews, rating updates, etc.) rather than for every run.

Apify gives you $5 in free usage credits every month on the [Apify Free Plan](https://apify.com/pricing). Depending on your review volume, you can monitor multiple locations completely free!

For regular monitoring of multiple business locations or higher review volumes, we recommend our [$49/month Starter plan](https://apify.com/pricing), which provides substantial event capacity.

For enterprise-level monitoring across many locations, the [$499 Scale plan](https://apify.com/pricing) offers unlimited capacity with premium support.

**Configured in the Apify Console** at the monetization/publish step (the platform no longer reads PPE pricing from `actor.json`). The events charged in code (`src/main.ts`) and their ratified tiers (`apifyMarginPercentage: 0.2`):

| Event | Charged on | FREE | BRONZE | SILVER | GOLD | PLATINUM | DIAMOND |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `apify-actor-start` *(synthetic, auto-charged by platform)* | once per run | $0.00005 | $0.00005 | $0.00005 | $0.00005 | $0.00005 | $0.00005 |
| `place-monitored` | per distinct place processed | $0.004 | $0.003 | $0.002 | $0.0015 | $0.001 | $0.0008 |
| `new-review` | per review **new since last run** | $0.003 | $0.002 | $0.0015 | $0.001 | $0.0006 | $0.0005 |
| `alert` | per alert emitted | $0.005 | $0.0035 | $0.0025 | $0.0018 | $0.0012 | $0.0008 |

> Do **not** define `apify-default-dataset-item` — it would auto-charge every pushed review and destroy the "charge on new only" moat. Reviews already seen are pushed to the default dataset uncharged. `apify-actor-start` is synthetic and must never be charged in code.

### Tips for monitoring Google Maps reviews

- Set up regular monitoring schedules (daily or weekly) to catch issues early before they escalate
- Configure alerts for rating drops to respond quickly to service issues
- Prioritize responding to unanswered reviews — this improves customer satisfaction and SEO
- Monitor competitor locations to benchmark your rating trends
- Export historical data to identify seasonal patterns or long-term trends
- Use negative review data to identify systemic service improvements needed

### The source interface (Phase-2 drop-in)

Every platform fetcher implements one contract (`src/sources/types.ts`):

```ts
interface ReviewSource {
  readonly platform: 'google_maps' | 'yelp' | 'tripadvisor' | 'booking' | 'amazon';
  fetchEntity(target, ctx): Promise<EntitySnapshot>;
  fetchReviews(target, opts, ctx): AsyncGenerator<RawReview>;
  canonicalReviewId(raw: RawReview): string; // stable per-source delta key
}
```

The pipeline never special-cases a platform; it resolves a source from `src/sources/registry.ts`. Adding a Phase-2 platform = implement one module + one line in the registry — no pipeline, state-model, dataset, or pricing change.

### Ethics & data

Reviews are public user-generated content. ReviewForge attributes each review to its original author, stores only public display data, and keeps `personalData` minimal — the delta key prefers the platform contributor id over any PII-derived hash. Respect each place's terms; use polite delays and residential proxies.

### Verifying locally

No Apify account, proxy, or credits required:

```bash
npm test                            # 28 unit tests (delta, bilingual sentiment)
npx tsx scripts/local-dry-run.ts    # end-to-end on the 30 real captured reviews
```

### Run locally

```bash
apify run          # reads storage/key_value_stores/default/INPUT.json
```

Local storage (`storage/`) is **not** synced to the Apify Console — inspect it locally or deploy with `apify push` and run on the platform.

### Project layout

```
.actor/                 # actor.json, input/output/dataset schemas
src/
  sources/              # source interface + google_maps reference impl + registry
  state.ts              # cross-run delta + atomic state (the pricing moat)
  sentiment.ts          # bilingual EN+ID keyword lexicon
  pipeline.ts           # platform-agnostic normalize → delta → sentiment → alerts
  orchestrator.ts       # the testable run core (per-target walk + watermark rules)
  main.ts               # Apify SDK shell: wires orchestrator to KV/datasets/charging
tests/                  # delta, sentiment, source contract, pipeline, orchestrator
spike/                  # S0 fetch-method spike (reference data + scripts)
```

> `main.ts` is a thin SDK adapter; every decision (which targets advance their watermark, what's fatal vs recoverable) lives in `orchestrator.ts` behind injectable ports, so the delta invariants are unit-tested without the SDK.

### Is it legal to scrape Google Maps?

Note that personal data is protected by GDPR in the European Union and by other regulations around the world.
You should not scrape personal data unless you have a legitimate reason to do so.
If you're unsure whether your reason is legitimate, consult your lawyers.

For business owners and reputation managers monitoring their own locations or locations they manage, this use case is generally legitimate.
However, scraping competitors' reviews may have legal and ethical considerations depending on your jurisdiction.

We also recommend that you read our blog post: [is web scraping legal?](https://blog.apify.com/is-web-scraping-legal/).

# Actor input Schema

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

Google Maps place URLs or place\_id values (e.g. ChIJN1t\_tDeuEmsRU...). Required unless searchQueries is given.

## `searchQueries` (type: `array`):

e.g. 'dentists in Bandung'. Resolved to places (capped by maxPlaces). Alternative entry to startUrls.

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

Maximum distinct places processed per run.

## `reviewsPerPlace` (type: `integer`):

Reviews fetched per place per run (newest-first).

## `lookbackDays` (type: `integer`):

P1 backfill guard (§6.4 / §11A·P1). On a place's FIRST run every fetched review is 'new'; this bounds the blast radius. Native fetch support = reviewsStartDate = now − lookbackDays. Default 30.

## `enableDelta` (type: `boolean`):

Compare fetched reviews against the last run's stored state (the monitoring core).

## `alertOnNewReviews` (type: `boolean`):

Emit an alert for every review new since the last run.

## `negativeReviewMaxRating` (type: `integer`):

Alert when a new review's rating is at or below this (1–5).

## `ratingDropThreshold` (type: `number`):

Alert if a place's rating drops by at least this vs the last snapshot.

## `trackUnanswered` (type: `boolean`):

Flag new reviews that have no owner response.

## `languages` (type: `array`):

Optional — only keep reviews in these languages (e.g. \['en','id']). Empty = all.

## `sentiment` (type: `string`):

MVP ships the bilingual keyword lexicon; 'api' is a v2 stub.

## `webhookUrl` (type: `string`):

POST each emitted alert to this URL if set (email/SMS deferred to v2 — wire Make/Zapier).

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

Residential proxies are recommended for Google (§4, §9). Standing default pre-fills the Apify residential pool.

## Actor input object example

```json
{
  "startUrls": [
    "https://www.google.com/maps/place/Single+Fin+Bali/@-8.8148568,115.0888711,17z/data=!3m1!4b1!4m6!3m5!1s0x2dd24ff9434fa3d1:0x73306bab5b47f9b4!8m2!3d-8.8148568!4d115.0888711!16s%2Fg%2F11b6zxmmq7"
  ],
  "searchQueries": [],
  "maxPlaces": 10,
  "reviewsPerPlace": 100,
  "lookbackDays": 30,
  "enableDelta": true,
  "alertOnNewReviews": true,
  "negativeReviewMaxRating": 3,
  "ratingDropThreshold": 0.3,
  "trackUnanswered": true,
  "languages": [],
  "sentiment": "keyword",
  "webhookUrl": "",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `reviews` (type: `string`):

Delta-tagged review records — the monitor's primary output. Only reviews new since last run are charged (the §7 moat).

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

Per-run scorecard returned from the run: places / reviewsFetched / newReviews / alertsByType.

# 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 = {
    "startUrls": [
        "https://www.google.com/maps/place/Single+Fin+Bali/@-8.8148568,115.0888711,17z/data=!3m1!4b1!4m6!3m5!1s0x2dd24ff9434fa3d1:0x73306bab5b47f9b4!8m2!3d-8.8148568!4d115.0888711!16s%2Fg%2F11b6zxmmq7"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("actorify/reviewforge").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 = {
    "startUrls": ["https://www.google.com/maps/place/Single+Fin+Bali/@-8.8148568,115.0888711,17z/data=!3m1!4b1!4m6!3m5!1s0x2dd24ff9434fa3d1:0x73306bab5b47f9b4!8m2!3d-8.8148568!4d115.0888711!16s%2Fg%2F11b6zxmmq7"],
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("actorify/reviewforge").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 '{
  "startUrls": [
    "https://www.google.com/maps/place/Single+Fin+Bali/@-8.8148568,115.0888711,17z/data=!3m1!4b1!4m6!3m5!1s0x2dd24ff9434fa3d1:0x73306bab5b47f9b4!8m2!3d-8.8148568!4d115.0888711!16s%2Fg%2F11b6zxmmq7"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call actorify/reviewforge --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=actorify/reviewforge",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "ReviewForge — Google Maps Reviews & Reputation Monitor",
        "description": "Monitoring-first Google Maps reviews actor: tells you what changed since last run — new reviews, rating drops, negative reviews, and unanswered reviews — instead of a one-shot bulk dump. Cross-run state + alerts. PAY_PER_EVENT.",
        "version": "0.1",
        "x-build-id": "SBiCM4NSg8zFb48zM"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/actorify~reviewforge/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-actorify-reviewforge",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/actorify~reviewforge/runs": {
            "post": {
                "operationId": "runs-sync-actorify-reviewforge",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/actorify~reviewforge/run-sync": {
            "post": {
                "operationId": "run-sync-actorify-reviewforge",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "properties": {
                    "startUrls": {
                        "title": "Start URLs / place IDs",
                        "type": "array",
                        "description": "Google Maps place URLs or place_id values (e.g. ChIJN1t_tDeuEmsRU...). Required unless searchQueries is given.",
                        "items": {
                            "type": "string",
                            "minLength": 1
                        },
                        "default": []
                    },
                    "searchQueries": {
                        "title": "Search queries",
                        "type": "array",
                        "description": "e.g. 'dentists in Bandung'. Resolved to places (capped by maxPlaces). Alternative entry to startUrls.",
                        "items": {
                            "type": "string",
                            "minLength": 1
                        },
                        "default": []
                    },
                    "maxPlaces": {
                        "title": "Maximum places per run",
                        "minimum": 1,
                        "maximum": 200,
                        "type": "integer",
                        "description": "Maximum distinct places processed per run.",
                        "default": 10
                    },
                    "reviewsPerPlace": {
                        "title": "Reviews per place",
                        "minimum": 1,
                        "maximum": 500,
                        "type": "integer",
                        "description": "Reviews fetched per place per run (newest-first).",
                        "default": 100
                    },
                    "lookbackDays": {
                        "title": "First-run lookback (days)",
                        "minimum": 1,
                        "maximum": 365,
                        "type": "integer",
                        "description": "P1 backfill guard (§6.4 / §11A·P1). On a place's FIRST run every fetched review is 'new'; this bounds the blast radius. Native fetch support = reviewsStartDate = now − lookbackDays. Default 30.",
                        "default": 30
                    },
                    "enableDelta": {
                        "title": "Enable cross-run delta",
                        "type": "boolean",
                        "description": "Compare fetched reviews against the last run's stored state (the monitoring core).",
                        "default": true
                    },
                    "alertOnNewReviews": {
                        "title": "Alert on new reviews",
                        "type": "boolean",
                        "description": "Emit an alert for every review new since the last run.",
                        "default": true
                    },
                    "negativeReviewMaxRating": {
                        "title": "Negative-review max rating",
                        "minimum": 1,
                        "maximum": 5,
                        "type": "integer",
                        "description": "Alert when a new review's rating is at or below this (1–5).",
                        "default": 3
                    },
                    "ratingDropThreshold": {
                        "title": "Rating-drop threshold",
                        "minimum": 0,
                        "maximum": 5,
                        "type": "number",
                        "description": "Alert if a place's rating drops by at least this vs the last snapshot.",
                        "default": 0.3
                    },
                    "trackUnanswered": {
                        "title": "Track unanswered reviews",
                        "type": "boolean",
                        "description": "Flag new reviews that have no owner response.",
                        "default": true
                    },
                    "languages": {
                        "title": "Language filter",
                        "type": "array",
                        "description": "Optional — only keep reviews in these languages (e.g. ['en','id']). Empty = all.",
                        "items": {
                            "type": "string",
                            "minLength": 1
                        },
                        "default": []
                    },
                    "sentiment": {
                        "title": "Sentiment method",
                        "enum": [
                            "none",
                            "keyword",
                            "api"
                        ],
                        "type": "string",
                        "description": "MVP ships the bilingual keyword lexicon; 'api' is a v2 stub.",
                        "default": "keyword"
                    },
                    "webhookUrl": {
                        "title": "Webhook URL",
                        "type": "string",
                        "description": "POST each emitted alert to this URL if set (email/SMS deferred to v2 — wire Make/Zapier).",
                        "default": ""
                    },
                    "proxyConfiguration": {
                        "title": "Proxy configuration",
                        "type": "object",
                        "description": "Residential proxies are recommended for Google (§4, §9). Standing default pre-fills the Apify residential pool."
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
