# Yelp Review + Owner-Response Incident Monitor (`herazur/yelp-review-owner-response-incident-monitor`) Actor

Monitor Yelp reviews and owner responses. Detect new negative reviews, unanswered complaints, service, price, quality and wait-time incidents, response delays, rating changes, and complaint spikes.

- **URL**: https://apify.com/herazur/yelp-review-owner-response-incident-monitor.md
- **Developed by:** [Furkan Toluç](https://apify.com/herazur) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 business checkeds

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

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

## What's an Apify Actor?

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

## Yelp Review + Owner-Response Incident Monitor

Monitor Yelp reviews for new 1–2 star incidents, unanswered complaints, slow owner responses, rating drops, complaint patterns, and review spikes. Stateful monitoring returns only meaningful changes since the previous run.

This Actor is a reputation monitor, not a generic Yelp data dump. Schedule it hourly or daily and connect its event dataset to Apify webhooks, APIs, Make, Zapier, or your own incident workflow.

### What you can monitor

- **Reputation monitoring:** detect newly published damaging reviews without reprocessing the same history as new incidents.
- **Owner-response SLA:** identify low-star reviews that remain unanswered beyond your chosen threshold and detect replies added later.
- **Agency and multi-location monitoring:** check many client or franchise pages in one fault-tolerant run.
- **Competitor monitoring:** watch public rating, review-count, and complaint changes on competitor Yelp pages.
- **Complaint intelligence:** tag repeated service, wait-time, price, quality, cleanliness, delivery, and order problems without an AI key.
- **Scheduled monitoring:** use Apify Schedules to run the same input hourly or daily; monitoring state stays in the default Key-Value Store.

### Example input

```json
{
  "businesses": [
    {
      "name": "Example Restaurant",
      "url": "https://www.yelp.com/biz/example-restaurant"
    }
  ],
  "maxReviewsPerBusiness": 100,
  "onlyChanges": true,
  "maximumIncidentRating": 2,
  "unansweredAfterHours": 48,
  "detectSignals": true,
  "includeReviewText": true,
  "monitorOwnerResponses": true,
  "stateStoreName": "yelp-reputation-monitor-state"
}
```

`encBizId` is an optional advanced field on a business. It skips the one business-page resolution request when you already know the ID publicly embedded by Yelp. The URL remains the stable monitoring key. Supplying it is the most reliable and least expensive production mode because Yelp may withhold business-page HTML even from Residential proxy sessions.

### Event output

With `onlyChanges: true`, Dataset rows are changes and incidents rather than a giant raw review dump:

```json
{
  "eventId": "evt_6f2c84d1c19f7c11e62feebdc91f0a9a",
  "eventType": "UNANSWERED_NEGATIVE_REVIEW",
  "severity": "HIGH",
  "baselineRun": false,
  "businessName": "Example Restaurant",
  "businessUrl": "https://www.yelp.com/biz/example-restaurant",
  "reviewId": "public-yelp-review-id",
  "reviewUrl": "https://www.yelp.com/biz/example-restaurant?hrid=public-yelp-review-id",
  "rating": 1,
  "reviewDate": "2026-08-26T03:44:25.000Z",
  "reviewText": "The manager was rude and we waited almost an hour.",
  "ownerResponseExists": false,
  "responseDelayHours": null,
  "signals": ["SERVICE_COMPLAINT", "WAIT_TIME_COMPLAINT"],
  "detectedAt": "2026-08-29T09:00:00.000Z"
}
```

Every event has a deterministic ID where the source transition permits it. The Dataset Console view puts event type, severity, business, rating, response status, signals, and detection time first.

### Incidents

| Event | Trigger |
| --- | --- |
| `NEW_NEGATIVE_REVIEW` | A newly discovered review is at or below `maximumIncidentRating`. |
| `NEW_REVIEW` | A newly discovered review is above the negative threshold. |
| `OWNER_RESPONSE_ADDED` | A known review changed from no public owner response to a response. |
| `UNANSWERED_NEGATIVE_REVIEW` | A negative review crossed `unansweredAfterHours` without a response. Emitted once. |
| `SLOW_OWNER_RESPONSE` | A newly added response has reliable timestamps and exceeded the SLA. |
| `RATING_DROP` / `RATING_INCREASE` | Yelp's public aggregate rating changed between successful runs. |
| `REVIEW_COUNT_CHANGED` | Yelp's public aggregate review count changed. |
| `REVIEW_VELOCITY_SPIKE` | New reviews are at least `max(5, 3 × recent run average)` after two comparison runs. |
| `NEGATIVE_REVIEW_SPIKE` | The same heuristic applied only to reviews at or below the negative threshold. |

Severity is deterministic. A new 1-star review, a cleanliness/safety signal, or at least three new negative reviews in one run is `CRITICAL`; negative SLA breaches and rating drops are `HIGH`; slow replies and general velocity spikes are `MEDIUM`; informational changes are `LOW`.

### Complaint signals

The classifier matches curated phrases with positive-context and negation safeguards. Its `confidence` field is a **heuristic confidence score**, not a machine-learning probability. Evidence snippets are deliberately short. No OpenAI, Anthropic, Gemini, embeddings, or external AI service is used.

Supported signals:

- `SERVICE_COMPLAINT`
- `WAIT_TIME_COMPLAINT`
- `PRICE_COMPLAINT`
- `QUALITY_COMPLAINT`
- `CLEANLINESS_COMPLAINT`
- `DELIVERY_OR_ORDER_COMPLAINT`

### Baseline and recurring state

The first successful check for each URL creates a baseline in `MONITORING_STATE_V1` in the named Key-Value Store selected by `stateStoreName`. A named store is essential because each Actor run receives a fresh default store. Use a distinct name when separate Tasks must keep independent baselines.

- With `onlyChanges: true`, the baseline is quiet: historical reviews are stored but are not described as newly occurring incidents.
- Historical negative reviews already older than the SLA are marked as baseline-known, so they do not suddenly fire on run two.
- With `onlyChanges: false`, current records are emitted as `REVIEW_SNAPSHOT` rows clearly marked `baselineRun: true` on the first run.
- A later `NO RESPONSE → RESPONSE EXISTS` transition emits `OWNER_RESPONSE_ADDED`.
- Only successful business checks replace that business's state. One failed URL does not erase or advance its last known-good baseline.

To start a genuinely new baseline, use a new Key-Value Store or delete the `MONITORING_STATE_V1` record intentionally.

### Efficient recurring checks

Reviews are requested newest-first in 10-review cursor pages. A recurring check stops at the first of:

- `maxReviewsPerBusiness`;
- the optional date cutoff;
- Yelp's end-of-feed cursor; or
- 20 consecutive review IDs already present in state.

This keeps a business with thousands of historical reviews inexpensive after its baseline. The PPE-ready commercial unit is one successfully persisted `business_checked` event; Store pricing must be configured in Apify Console before charging is activated.

### OUTPUT summary

The run's default Key-Value Store record `OUTPUT` includes run status, baseline status, the persistent state-store name, business success/failure counts, reviews and pages fetched, each major incident count, complaint-signal totals, timestamps, and actionable per-business failures. If every business fails, the Actor writes `OUTPUT` and fails the run instead of silently reporting an empty success.

### Extraction method and public-data scope

The Actor reads only information publicly displayed by Yelp:

1. One HTTP request resolves a Yelp `/biz/` URL through the page's public `yelp-biz-id` metadata. If Yelp challenges that lightweight request, the Actor retries the public page in a fingerprinted browser with fresh proxy sessions.
2. Yelp's logged-out frontend review feed is called directly over HTTP with `GetBusinessReviewFeed`.
3. Structured review IDs, rating, timestamps, full public text, minimal public reviewer display fields, aggregate rating/count, and `bizUserPublicReply` are normalized.

It does not log in, solve CAPTCHAs, access a Yelp for Business inbox, write reviews, use private user data, or attempt to deanonymize reviewers. Reviewer metadata is intentionally limited to the public display name and location needed for stable fallback IDs and human-readable incidents.

### Proxy and access limitations

Yelp actively challenges automated and datacenter traffic. The input defaults to an Apify Residential US proxy because URL-to-business-ID resolution is otherwise frequently blocked. The structured review feed is usually cheaper and more permissive, but it can also be rate-limited.

- A residential proxy is recommended for Store and production runs; its usage is billed by Apify.
- No hard-coded proxy credentials or external API secrets are required.
- URL-only inputs depend on Yelp allowing the one public business-page resolution request. If all rotated browser sessions receive a DataDome/challenge shell, supply that page's public `encBizId`; review and owner-response monitoring then runs through the lightweight structured feed.
- Browser fallback rotates up to three isolated proxy sessions. If Yelp continues to serve a challenge, redirects away from `/biz/`, changes its persisted GraphQL document, or changes the response shape, that business fails with a diagnostic error.
- The Actor does not bypass a CAPTCHA or retry through an authentication wall.
- Public owner-response dates are nullable. Response-delay and slow-response events are emitted only when both dates parse reliably.
- Owner-response transitions can only be detected for reviews inside the configured recent-review inspection window. Increase `maxReviewsPerBusiness` if businesses commonly reply to older reviews.
- Yelp may alter or remove reviews between runs. The MVP detects additions and response transitions; it does not claim that an absent review was deleted when the recent-page cap may simply have excluded it.
- Business category and address metadata come from the permitted business page and can be null when Yelp omits it. Aggregate rating and count are taken from the structured review feed when available.

Review Yelp's terms and applicable law for your use case. You are responsible for having a lawful purpose for processing public review data.

### Scheduling

Create an Apify Task with your input, run it once to establish the baseline, then create an Apify Schedule for that Task. Use the Dataset API or a webhook on successful runs to forward incident rows. Keep the same `stateStoreName` on every scheduled run; the Actor opens that named store explicitly, while each run still writes its own default `OUTPUT` record.

### Local development

```bash
npm install
npm run build
npm test
npm run smoke:live
```

The live smoke command uses public Yelp IDs paired with their business URLs so it can validate review pagination without the challenged alias-resolution request. Full URL-only validation should be run on Apify with the default Residential proxy before Store publication.

# Actor input Schema

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

Public Yelp business URLs to monitor. Add an optional display name. Advanced users may provide the public page's Yelp business ID to skip URL resolution.

## `maxReviewsPerBusiness` (type: `integer`):

Maximum number of recent reviews inspected during each run.

## `onlyChanges` (type: `boolean`):

Return only newly discovered reviews and reputation incidents since the previous successful run. The first run establishes a quiet baseline.

## `maximumIncidentRating` (type: `integer`):

Reviews at or below this rating are treated as negative incidents.

## `unansweredAfterHours` (type: `integer`):

How long a negative review may remain unanswered before one incident is generated.

## `detectSignals` (type: `boolean`):

Detect service, wait-time, price, quality, cleanliness, and order-related complaints with deterministic phrase matching.

## `includeReviewText` (type: `boolean`):

Include publicly visible review and owner-response text in output events.

## `monitorOwnerResponses` (type: `boolean`):

Detect newly added public business-owner replies and response-SLA incidents.

## `stateStoreName` (type: `string`):

Named Key-Value Store used across scheduled runs. Use a different name to isolate separate monitoring tasks.

## `dateCutoff` (type: `string`):

Optional earliest review date to inspect, in YYYY-MM-DD format.

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

Yelp commonly challenges datacenter traffic. Apify Residential proxy with a US country is recommended for reliable URL resolution.

## Actor input object example

```json
{
  "businesses": [
    {
      "name": "The French Laundry",
      "url": "https://www.yelp.com/biz/the-french-laundry-yountville-7",
      "encBizId": "T20VEwi7AzKbY2TuVEt_ig"
    }
  ],
  "maxReviewsPerBusiness": 100,
  "onlyChanges": true,
  "maximumIncidentRating": 2,
  "unansweredAfterHours": 48,
  "detectSignals": true,
  "includeReviewText": true,
  "monitorOwnerResponses": true,
  "stateStoreName": "yelp-reputation-monitor-state",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}
```

# Actor output Schema

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

New Yelp review incidents and meaningful reputation changes, or review snapshots when onlyChanges is disabled.

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

Run status, processing totals, incident counts, complaint-signal totals, and per-business failures.

# 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("herazur/yelp-review-owner-response-incident-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 = {}

# Run the Actor and wait for it to finish
run = client.actor("herazur/yelp-review-owner-response-incident-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 '{}' |
apify call herazur/yelp-review-owner-response-incident-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,herazur/yelp-review-owner-response-incident-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/XK3dj1B7AcbOvrC3c/builds/zhRbCbjCKkntyhVc2/openapi.json
