# Yelp Review Scraper - fails instead of billing empty (`marekhartmann/yelp-review-scraper`) Actor

Scrape Yelp business reviews with ratings, dates, authors, owner replies and reaction counts. Returns an error instead of an empty, billable dataset when Yelp blocks or changes.

- **URL**: https://apify.com/marekhartmann/yelp-review-scraper.md
- **Developed by:** [Marek Hartmann](https://apify.com/marekhartmann) (community)
- **Categories:** Lead generation, E-commerce, Automation
- **Stats:** 2 total users, 1 monthly users, 64.3% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 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/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 Scraper

Scrape **Yelp business reviews** — star rating, full review text, review date, author profile
stats, owner replies and reaction counts — from any Yelp business page.

Built around one rule the alternatives break:

> **An empty result is a failure, not an invoice.**

### Why this Actor exists

Yelp's review feed answers `HTTP 200` even when the query did not work. A scraper that trusts the
status code will quietly hand you an empty dataset and charge you for the run. This Actor treats
every unexplained empty result as its own bug and **fails the run instead**.

It also tells apart *whose* problem it is:

| Situation | What happens | You pay |
|---|---|---|
| Business does not exist (typo in the URL) | Run **succeeds**, dataset empty, reason explained | Nothing (pay per result) |
| Business has zero reviews | Run **succeeds**, reason explained | Nothing |
| Your language / rating filter matches nothing | Run **succeeds**, reason explained | Nothing |
| Yelp blocked us, changed its API, or returned nonsense | Run **fails** loudly | Nothing |
| Reviews found | Run succeeds with data | Per result |

Your typo never fails a run, so it never dents this Actor's success rate — and our bug never
turns into your invoice.

### Input

```json
{
  "startUrls": ["https://www.yelp.com/biz/gary-danko-san-francisco"],
  "maxReviewsPerBusiness": 100,
  "sortBy": "DATE_DESC",
  "ratings": [],
  "languageCode": "en",
  "proxyConfiguration": { "useApifyProxy": true, "apifyProxyGroups": ["UNBLOCKER"] }
}
```

| Field | Meaning |
|---|---|
| `startUrls` | Yelp business URLs, or bare aliases like `gary-danko-san-francisco` |
| `maxReviewsPerBusiness` | Hard cap per business (default 100) |
| `sortBy` | `RELEVANCE_DESC`, `DATE_DESC`, `DATE_ASC`, `RATING_DESC`, `RATING_ASC`, `ELITES_DESC` |
| `ratings` | Empty = all. `[1, 2]` = negative reviews only |
| `languageCode` | Review language, default `en` |
| `proxyConfiguration` | **Use UNBLOCKER.** Verified live: residential proxy returns HTTP 403 even with a browser fingerprint |

### Output

One row per review:

```json
{
  "reviewId": "O3-ogBbIo1nuz0W5gSylkQ",
  "url": "https://www.yelp.com/biz/gary-danko-san-francisco?hrid=O3-ogBbIo1nuz0W5gSylkQ",
  "businessAlias": "gary-danko-san-francisco",
  "businessName": "Gary Danko",
  "rating": 5,
  "text": "Full review text, not truncated.",
  "language": "en",
  "publishedAt": "2026-08-16T22:41:32-07:00",
  "publishedDate": "2026-08-16",
  "authorName": "Jane D.",
  "authorId": "78eKGjfH17lXvnvRS-3Odg",
  "authorLocation": "San Francisco, CA",
  "authorReviewCount": 42,
  "authorFriendCount": 7,
  "authorIsElite": true,
  "authorPhotoUrl": "https://s3-media0.fl.yelpcdn.com/photo/.../30s.jpg",
  "photoUrls": [],
  "helpfulCount": 3,
  "thanksCount": 0,
  "loveThisCount": 0,
  "ohNoCount": 0,
  "ownerReply": null,
  "ownerReplyAt": null,
  "isFirstReview": false,
  "scrapedAt": "2026-08-19T17:00:00.000Z"
}
```

Every run also writes a **`RUN_REPORT`** record to the key-value store: what was requested, what
came back, and — if something went wrong — which side it was on.

### Speed

The Actor pulls **40 reviews per request** (Yelp's own page does 10) and builds pagination cursors
itself, so 100 reviews cost 3 requests instead of 10. Fewer requests also means less proxy burn.

### Use cases

- Reputation monitoring for a restaurant, dentist, salon or hotel
- Competitor review analysis and sentiment scoring
- Feeding review text into an LLM for theme extraction
- Lead generation from businesses with poor recent ratings
- Academic and market research on local business reviews

### Limits and honesty

- Yelp sits behind DataDome. Measured on live runs: a datacenter IP fails, and so does a US residential IP with a full browser fingerprint (HTTP 403). Apify's UNBLOCKER proxy gets through — 100 reviews in 39 seconds. This is a cost line, not a checkbox.
- Yelp rotates the hash of its persisted GraphQL query from time to time. When that happens this
  Actor **fails with `API_DRIFT`** rather than returning nothing; the fix is a version bump, and
  `reviewFeedDocumentId` lets you patch it yourself in the meantime.
- Only publicly visible reviews. Nothing behind a login, no private data.

### Tests

```bash
npm install
npm test
```

28 tests, no network needed: input parsing, cursor maths, blocked-page detection, GraphQL error
classification, and every branch of the "whose fault is it" decision.

### Need a scraper for something else?

I build custom Apify Actors and browser automation the same way this one is built:
a verified result or an explicit failure, never an empty dataset sold as a success.

- My other Actors: https://apify.com/marekhartmann
- Code and test suites: https://github.com/marekhartmann-creator

Tell me the site and what you need out of it.

# Actor input Schema

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

Business pages to scrape, e.g. https://www.yelp.com/biz/gary-danko-san-francisco. A plain alias works too.

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

Upper limit of reviews scraped per business.

## `sortBy` (type: `string`):

Yelp's own sort orders.

## `ratings` (type: `array`):

Leave empty for all ratings. Example: \[1, 2] for negative reviews only.

## `languageCode` (type: `string`):

Two-letter code of the review language, e.g. en, de, fr. A language with no reviews returns an explained empty result, not a charge.

## `reviewFeedDocumentId` (type: `string`):

Only needed if Yelp rotates its persisted query hash before this Actor is updated. Leave empty.

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

Yelp is behind DataDome. Verified live: RESIDENTIAL returns HTTP 403 even with a browser fingerprint; UNBLOCKER gets through. Do not change this unless you know better.

## Actor input object example

```json
{
  "startUrls": [
    "https://www.yelp.com/biz/gary-danko-san-francisco"
  ],
  "maxReviewsPerBusiness": 100,
  "sortBy": "RELEVANCE_DESC",
  "ratings": [],
  "languageCode": "en",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "UNBLOCKER"
    ]
  }
}
```

# Actor output Schema

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

One row per review: rating, full text, date, author profile stats, owner reply and reaction counts.

## `runReport` (type: `string`):

Per-business outcome and, when something went wrong, whose side it was on: your input or ours.

# 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.yelp.com/biz/gary-danko-san-francisco"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("marekhartmann/yelp-review-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 = { "startUrls": ["https://www.yelp.com/biz/gary-danko-san-francisco"] }

# Run the Actor and wait for it to finish
run = client.actor("marekhartmann/yelp-review-scraper").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.yelp.com/biz/gary-danko-san-francisco"
  ]
}' |
apify call marekhartmann/yelp-review-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,marekhartmann/yelp-review-scraper"
        }
    }
}

```

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/1pshEKgMtq0HjdF2T/builds/ac2aTtnqCLVFeSnwt/openapi.json
