# Review Monitor: Negative Review Alerts and Rating Drift (`arcadium/reputation-monitor`) Actor

New 1-2★ reviews, rating drift, owner-reply rate and review velocity per watched business, app or SaaS product since your last run; $12 per 1,000 checks. Alerts $2 per 1,000. 11 sources incl. Booking, Tripadvisor, Google Maps, Yelp, app stores, Capterra. No reviewer names.

- **URL**: https://apify.com/arcadium/reputation-monitor.md
- **Developed by:** [Arcadium Data](https://apify.com/arcadium) (community)
- **Categories:** Marketing, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $12.00 / 1,000 entity checkeds

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?

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

## Review Monitor: New Negative Reviews and Rating Drift

Give it a watchlist of hotels, restaurants and local businesses, apps or SaaS products (their page URLs on Booking.com, Agoda, Trip.com, Tripadvisor, Google Maps, Google Hotels, Yelp, Google Play, the App Store, Capterra or TrustRadius) and every run tells you what changed since the last run. You get one alert row for each new 1★ or 2★ review and one summary row per entity with rating drift, the 30-day average against the lifetime average, owner-reply rate and reply lag, review velocity and, for apps, a per-version rating regression.

All sources come back in one feed with one row shape, deduplicated, with no reviewer names or avatars. Schedule it daily and connect a webhook, Slack or a sheet through Apify integrations.

It is for hotel groups and franchises watching many locations, agencies reporting on clients' reputation, app teams who need to know when a release hurts the rating, and product marketers tracking their SaaS product next to competitors.

### Use with AI agents

Returns new negative reviews and reputation metrics (rating drift, 30-day vs lifetime average, owner-reply rate and lag, velocity, app-version regressions) per watched URL since the previous run. Agents can call it through the Apify MCP server (`https://mcp.apify.com`, open source as `apify/actors-mcp-server`) as `arcadium/reputation-monitor`: the input schema is read from the actor, the result is a dataset of JSON rows.

Input example (minimal):

```json
{
  "startUrls": [
    "https://www.booking.com/hotel/gb/the-savoy.html",
    "https://www.yelp.com/biz/katzs-delicatessen-new-york",
    "https://apps.apple.com/us/app/slack/id618783545"
  ],
  "stateKey": "my-watchlist"
}
```

What comes back: one `summary` row per entity and one `alert` row per new review rated at or below `alertMaxRating` (default 2) since the last run with the same `stateKey`. Filter on the `rowType` field; `flags` on the summary row lists what needs attention (`new_negative`, `rating_drop`, `recent_below_lifetime`, `version_regression`, `unanswered_negative`, `velocity_spike`). **The first run per entity is a baseline**: it records the newest reviews as history and returns only the summary (`status: "baseline"`) at the cheaper baseline price; alerts start with the second run. A URL that fails gets a free `error` row with a `code` (`invalid_input`, `not_found`, `blocked`, `unavailable` or `no_match`), a `message` and the `input`. Retry on `blocked` and `unavailable` later; do not retry the other codes with the same input.

Cost of a daily check of 100 entities with 3 new negative reviews in total: 100 checks at $12 per 1,000 plus 3 alerts at $2 per 1,000 is about $1.21 per run on the free plan. The first run of the same list costs about $0.90 (baseline). Error rows are never charged.

### What you get

Summary row, one per entity and run:

| Field | What it holds |
| --- | --- |
| `status` | `baseline` (first run), `changed` (new or edited reviews) or `unchanged`. |
| `rating`, `previousRating`, `ratingChange` | The source's average now, at the previous run, and the drift in stars. |
| `reviewCount`, `reviewCountChange` | Stated review count (app stores: star ratings) and how much it grew. |
| `newReviews`, `newNegativeReviews`, `newReviewsAvgRating` | Reviews first seen this run, how many became alerts, and their average. |
| `avgRating30d`, `avgRatingLifetime`, `rating30dVsLifetime` | Recent vs usual. Negative = recent reviews are worse. |
| `reviews7d`, `reviews30d`, `reviewsPrior30d`, `reviewsPerWeek` | Velocity. |
| `ownerReplyRate90d`, `medianReplyLagDays`, `unansweredNegative30d` | Reply rate of the last 90 days, median days to reply, negative reviews still without a reply. |
| `appVersions`, `versionRegression` | Apps: rating per version, and the newest version when it averages 0.5+ stars below the one before. |
| `windowComplete30d` | `true` when the remembered history reaches back 30 days, so the 30-day figures are exact. |
| `flags` | What needs attention (empty on a baseline). |

Output example:

```json
{
  "rowType": "summary",
  "status": "changed",
  "platform": "appstore",
  "entityId": "618783545-us",
  "input": "https://apps.apple.com/us/app/slack/id618783545",
  "name": "Slack",
  "url": "https://apps.apple.com/us/app/slack/id618783545",
  "checkedAt": "2026-10-21T22:59:28.653Z",
  "previousCheckAt": "2026-10-21T22:54:50.343Z",
  "rating": 4.14,
  "previousRating": 4.14,
  "ratingChange": 0,
  "reviewCount": 54143,
  "previousReviewCount": 54115,
  "reviewCountChange": 28,
  "newReviews": 6,
  "editedReviews": 0,
  "newNegativeReviews": 1,
  "newReviewsAvgRating": 4.5,
  "avgRating30d": 3.29,
  "previousAvgRating30d": 3.29,
  "reviews7d": 55,
  "reviews30d": 100,
  "reviewsPerWeek": 23.3,
  "avgRatingLifetime": 3.29,
  "lifetimeSource": "sample",
  "rating30dVsLifetime": 0,
  "ownerReplyRate90d": 0.05,
  "medianReplyLagDays": 0,
  "unansweredNegative30d": 35,
  "appVersions": [],
  "versionRegression": null,
  "windowComplete30d": false,
  "historyReviews": 100,
  "historySince": "2026-10-08",
  "stopReason": "onlyNew",
  "flags": ["new_negative", "unanswered_negative"]
}
```

Alert row, one per new negative review:

```json
{
  "rowType": "alert",
  "platform": "appstore",
  "entityId": "618783545-us",
  "entityName": "Slack",
  "entityUrl": "https://apps.apple.com/us/app/slack/id618783545",
  "reviewId": "AppStore___14688767912",
  "date": "2026-10-20",
  "rating": 2,
  "title": "Issue",
  "text": "A lot of issues when bringing up certain things they don't bring them up in order unless you put that you want newest. ...",
  "appVersion": null,
  "ownerReplied": true,
  "ownerReply": "Ah, apologies, we're working to tackle this bug. ...",
  "changeType": "new",
  "checkedAt": "2026-10-21T22:59:28.653Z"
}
```

`changeType: "edited"` marks a review you already knew that its author edited down into the alert band.

### Input

| Field | What it does |
| --- | --- |
| `startUrls` | One URL per entity: Booking.com, Agoda or Trip.com hotel, Tripadvisor hotel/restaurant/attraction, Google Hotels hotel, Google Maps place (restaurant, shop, clinic, any business), Yelp business, Google Play app, App Store app, Capterra product, TrustRadius product. Up to 2,000. |
| `stateKey` | Names the watchlist. Each run is compared with the last run of the same name. A new name starts a new baseline. |
| `alertMaxRating` | Alert at or below this many stars (1-4, default 2). Booking, Agoda, Trip.com and TrustRadius 1-10 scores are halved first. |
| `maxReviewsPerEntity` | Newest reviews read per entity and run, default 100. Later runs stop at the last review seen, usually after one page. |
| `language`, `country` | Google Play review language (default `en`) and app store country (default `us`). |
| `includeErrorRows` | Free error rows, on by default. |

#### How the comparison works

The actor keeps two small records per entity and watchlist in your account: the ids of the reviews it has seen (`reputation-monitor-seen`) and a history of review facts (date, rating, replied, reply date, app version; no text) in `reputation-monitor-state`. Each run reads the newest reviews until it reaches ones it has seen, writes the rows and saves the new state.

- The first run per entity and watchlist only records the history: no alerts, cheaper price.
- A reply that arrives days after the review is picked up on the next run that still reads that review (the run re-reads the last 7 days).
- If a run stops early (your maximum cost per run), the reviews that were not alerted stay unseen and come with the next run.
- The same entity under two URLs is checked once, and each review is reported once.

#### Alerts: webhook, Slack, sheets

Use Apify's own integrations on a schedule: add a webhook for "Run succeeded" that posts the dataset link to your endpoint, or use the Slack, Google Sheets, Zapier or Make integrations. Filter on `rowType: "alert"`, or on summary rows whose `flags` are not empty.

### Use it from an AI assistant

```text
Use the Apify actor "arcadium/reputation-monitor" to watch these places and apps.
Input: {"startUrls":["https://www.booking.com/hotel/gb/the-savoy.html","https://www.yelp.com/biz/katzs-delicatessen-new-york"],"stateKey":"daily"}
Rows with rowType "alert" are new 1-2 star reviews. Rows with rowType "summary" have rating, ratingChange,
avgRating30d, ownerReplyRate90d and flags per entity. The first run is a baseline with no alerts.
Run it, then summarise what went wrong in the new negative reviews and which entity needs a reply first.
```

### Use it from code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("arcadium/reputation-monitor").call(run_input={
    "startUrls": ["https://www.booking.com/hotel/gb/the-savoy.html"],
    "stateKey": "daily",
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    if row["rowType"] == "alert":
        print(row["entityName"], row["rating"], row["text"][:80])
```

JavaScript:

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('arcadium/reputation-monitor').call({
  startUrls: ['https://apps.apple.com/us/app/slack/id618783545'],
  stateKey: 'daily',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.filter((i) => i.rowType === 'summary' && i.flags.length > 0));
```

### Pricing

You pay per entity checked and per alert row, at the price of your Apify plan. The first run per entity is a cheaper baseline. Error rows are free.

| Apify plan | Per 1,000 entity checks | Per 1,000 alerts | Per 1,000 baselines |
| --- | --- | --- | --- |
| Free | $12.00 | $2.00 | $9.00 |
| Starter (Bronze) | $12.00 | $2.00 | $9.00 |
| Scale (Silver) | $12.00 | $2.00 | $9.00 |
| Business (Gold) | $12.00 | $2.00 | $9.00 |
| Platinum | $12.00 | $2.00 | $9.00 |
| Diamond | $12.00 | $2.00 | $9.00 |

Plus a one-off $0.001 per GB of memory when a run starts.

Examples: one hotel checked daily for a month is 30 checks, $0.36 plus a few cents of alerts. 50 locations checked daily is about $18 a month. Set a maximum cost per run in the Apify run options and the actor stops before it goes over.

### Use cases

- Hotel groups and franchises: one feed for every location on Booking, Tripadvisor and Yelp; reply to the new 1★ reviews first, watch `ownerReplyRate90d` per location.
- Agencies: a weekly reputation report per client from the summary rows (`ratingChange`, `rating30dVsLifetime`, `reviewCountChange`).
- App teams: `versionRegression` shows when a release averages clearly lower than the one before it; alerts carry the `appVersion`.
- SaaS product marketing: watch your Capterra and TrustRadius pages and three competitors' in one watchlist.

### Limitations

- Public review pages only. Nothing behind a login.
- Supported: Booking.com, Agoda and Trip.com hotels (Agoda: its own reviews, not the Booking.com ones it also shows), Tripadvisor places, Google Hotels hotels and Google Maps places (Google's own reviews; dates are approximate because Google shows "3 months ago"), Yelp businesses, Google Play and App Store apps, Capterra and TrustRadius products. Other URLs return a free `invalid_input` error row.
- The 30-day, reply and velocity figures come from the reviews the actor has read. For entities with more reviews per month than `maxReviewsPerEntity` they describe the newest reviews read (`windowComplete30d: false`) until the history fills up over later runs.
- App stores state the average of all star ratings, most of them without text, so `avgRatingLifetime` there is the average of the written reviews read (`lifetimeSource: "sample"`).
- Reply lag needs a reply date: Tripadvisor, Agoda, Trip.com, Yelp and the app stores have one; Booking and Capterra show the reply only; TrustRadius has no owner replies.
- App versions: Google Play states the version on every review. The App Store does so only on its RSS feed, which the actor uses as a fallback, so App Store version data is usually empty.
- TrustRadius is read through residential proxies; the actor reads at most 3 pages (30 reviews) per TrustRadius product and run.
- A review that is deleted on the site is not reported.
- Entities are read one after another, about 5 seconds each (7 on the first run), so 700 entities take about an hour. The default run timeout is 4 hours; split bigger watchlists into several `stateKey`s run in parallel.

### Privacy

Reviewer names, avatars, profile links and similar fields are never collected. Email addresses and phone numbers inside review text are replaced by `[email]` and `[phone]`, and names in greetings ("Dear John") are removed. The saved history holds only review ids, dates, ratings, reply status and app versions.

### FAQ

**Why did the first run return no alerts?**
It is the baseline: there is nothing to compare with yet. It records the newest reviews per entity and returns one summary row each at the baseline price. Run it again tomorrow (or put it on a schedule) and the alerts come.

**How do I start over?**
Use a new `stateKey`. The old records stay in the `reputation-monitor-state` and `reputation-monitor-seen` stores until you delete them.

**Can I get every new review, not only the negative ones?**
Set `alertMaxRating` to 4 for everything below 5 stars, or use the per-site review actors with "only new reviews" for full review rows.

**Something looks wrong in the output. Who do I tell?**
Open an issue on the actor page with the run link. We read them all.

# Actor input Schema

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

Example: \["https://www.booking.com/hotel/gb/the-savoy.html", "https://play.google.com/store/apps/details?id=com.Slack"]. One URL per hotel, business, app or product. Sources: Booking.com, Agoda or Trip.com hotel, Tripadvisor place, Google Hotels hotel (google.com/travel/hotels/entity/...), Google Maps place (a place link, maps.app.goo.gl link or maps.google.com/?cid=...), Google Play app, App Store app, Yelp business, Capterra product, TrustRadius product. Up to 2,000 per run.

## `stateKey` (type: `string`):

Example: `hotels-london`. Names the saved state (1-40 letters, digits or dashes), so several watchlists run side by side. Each run is compared with the last run of the same name. A new name starts a new baseline.

## `alertMaxRating` (type: `integer`):

Example: 2 = one alert row per new 1★ or 2★ review. Allowed 1-4. Booking, Agoda, Trip.com and TrustRadius 1-10 scores are halved to the 1-5 scale first.

## `maxReviewsPerEntity` (type: `integer`):

Example: 100. Newest reviews read per entity and run; the first run uses them as history for the 30-day and reply metrics. Later runs stop at the last seen review, usually after one page. Raise it for apps with hundreds of reviews a day. 10-2,000.

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

Example: `en`. Google Play shows reviews per language (code such as en, de, pt-BR). Other sources return every language.

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

Example: `us`. Two-letter storefront for Google Play, and for App Store URLs without a country.

## `includeErrorRows` (type: `boolean`):

Default on. Adds a free `error` row (code + message) for every URL that could not be read or is not supported, so a failure is never silent.

## `maxRetries` (type: `integer`):

Example: 3. How often a request is retried after a temporary error (0-10).

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

Advanced: leave empty. Each source uses the cheapest proxy that works for it. Set your own proxy only if you must.

## Actor input object example

```json
{
  "startUrls": [
    "https://www.booking.com/hotel/gb/the-savoy.html",
    "https://www.yelp.com/biz/katzs-delicatessen-new-york",
    "https://apps.apple.com/us/app/slack/id618783545"
  ],
  "stateKey": "default",
  "alertMaxRating": 2,
  "maxReviewsPerEntity": 50,
  "language": "en",
  "country": "us",
  "includeErrorRows": true,
  "maxRetries": 3
}
```

# Actor output Schema

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

All rows of this run in the default dataset.

## `view_summary` (type: `string`):

No description

## `view_alerts` (type: `string`):

No description

## `view_replies` (type: `string`):

No description

## `view_apps` (type: `string`):

No description

## `view_errors` (type: `string`):

No description

## `record_stats` (type: `string`):

Requests, data volume, entities checked, alerts and cost per entity for this run.

# 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.booking.com/hotel/gb/the-savoy.html",
        "https://www.yelp.com/biz/katzs-delicatessen-new-york",
        "https://apps.apple.com/us/app/slack/id618783545"
    ],
    "maxReviewsPerEntity": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("arcadium/reputation-monitor").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.booking.com/hotel/gb/the-savoy.html",
        "https://www.yelp.com/biz/katzs-delicatessen-new-york",
        "https://apps.apple.com/us/app/slack/id618783545",
    ],
    "maxReviewsPerEntity": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("arcadium/reputation-monitor").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 '{
  "startUrls": [
    "https://www.booking.com/hotel/gb/the-savoy.html",
    "https://www.yelp.com/biz/katzs-delicatessen-new-york",
    "https://apps.apple.com/us/app/slack/id618783545"
  ],
  "maxReviewsPerEntity": 50
}' |
apify call arcadium/reputation-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,arcadium/reputation-monitor"
        }
    }
}
```

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/JaQlA1uLsEtY3hV7a/builds/xEtnse5u1K3X79oYZ/openapi.json
