# Yelp Reviews Scraper — Full History & Incremental Updates (`astravalabs/yelp-reviews-scraper`) Actor

Turn Yelp business URLs into structured review records: rating, full text, date, reviewer, photos, reactions, edit history and owner replies. Filter by star rating, or collect only new reviews with a since date.

- **URL**: https://apify.com/astravalabs/yelp-reviews-scraper.md
- **Developed by:** [astravalabs](https://apify.com/astravalabs) (community)
- **Categories:** AI, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.30 / 1,000 reviews

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?

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

## Yelp Reviews Scraper — Full History & Incremental Updates

Turn Yelp business URLs into one clean row per review: star rating, full text,
date, reviewer, photos and videos, reactions, edit history and the owner's
public reply. Collect a business's complete review history, or only what's new
since your last run. Proxies and retries are handled for you.

Use it for reputation monitoring, sentiment analysis, competitor research,
customer-service benchmarking and recurring review datasets.

### Quick start

```json
{
  "startUrls": [
    "https://www.yelp.com/biz/aji-kiji-san-francisco",
    "hotel-zetta-san-francisco-san-francisco-3"
  ],
  "sortBy": "DATE_DESC",
  "maxItemsPerTarget": 200
}
```

`startUrls` takes full Yelp business URLs or the bare alias, which is the part
after `/biz/`. Duplicates are removed before the run starts. Anything that isn't
a Yelp business is skipped and listed in the run report, so one bad entry
doesn't fail the batch.

### What you get

Each row in the default dataset is one review. The example below is
illustrative: the reviewer and ids are invented, but the shape is exact.

```json
{
  "reviewId": "q3Xr0bQe9vTnL1kZpA7mWg",
  "url": "https://www.yelp.com/biz/aji-kiji-san-francisco?hrid=q3Xr0bQe9vTnL1kZpA7mWg",
  "businessAlias": "aji-kiji-san-francisco",
  "businessId": "SIIBwgrR7Rl71mE_mVzdIQ",
  "businessName": "Aji Kiji",
  "rating": 4,
  "date": "2026-09-08T11:27:00-07:00",
  "text": "Glad they moved to FiDi so I could finally try this place...",
  "language": "en",
  "authorName": "Jordan M.",
  "authorId": "u8Kd2LmQx0RbT5vYcN1oZw",
  "authorProfileUrl": "https://www.yelp.com/user_details?userid=u8Kd2LmQx0RbT5vYcN1oZw",
  "authorPhotoUrl": "https://s3-media0.fl.yelpcdn.com/photo/.../180s.jpg",
  "authorLocation": "Daly City, CA",
  "authorReviewCount": 412,
  "authorFriendCount": 87,
  "authorPhotoCount": 230,
  "authorIsElite": true,
  "authorEliteYear": "26",
  "authorIsEliteAllStar": false,
  "photoCount": 1,
  "photos": [
    {
      "url": "https://s3-media0.fl.yelpcdn.com/bphoto/.../o.jpg",
      "caption": "Chirashi bowl",
      "width": 1536,
      "height": 2048,
      "uploadedAt": "2026-09-08T18:27:00Z"
    }
  ],
  "videoCount": 0,
  "videos": [],
  "checkInCount": 2,
  "isFirstReviewer": false,
  "isReviewOfTheDay": false,
  "hasPaidThroughYelp": false,
  "hasPurchasedDeal": false,
  "hasPlacedReservation": false,
  "reservationProvider": null,
  "reactions": { "HELPFUL": 3, "THANKS": 1, "LOVE_THIS": 1 },
  "previousVersionCount": 0,
  "hasBeenEdited": false,
  "previousReviewIds": [],
  "ownerReply": "Thanks so much for visiting, Jordan!",
  "ownerReplyDate": "2026-09-10T09:02:11-07:00",
  "ownerReplyAuthor": "Ken S.",
  "ownerReplyAuthorRole": "MANAGER",
  "expertTopic": "Sushi",
  "scrapedAt": "2026-10-01T21:57:42Z"
}
```

A few fields need explaining:

- **`date`** is the time the review was posted, in the business's local time
  with its UTC offset, exactly as Yelp states it. It is not converted to UTC,
  because a review posted on the evening of the 13th in San Francisco should
  still read as the 13th.
- **`hasBeenEdited`**, **`previousVersionCount`** and **`previousReviewIds`**
  show when a reviewer has rewritten their review. `text` is always the current
  version.
- **`authorIsElite`** and **`authorEliteYear`** match the "Elite 26" badge on
  the reviewer's profile. **`authorIsEliteAllStar`** is the extra "All-Star"
  badge some Elite reviewers also carry.
- **`reactions`** lists only the reactions a review actually received.
- **`photos`** link to the full-size originals, not thumbnails.
- **`ownerReply`** and its date, author and role are filled only when the
  business has replied publicly.

The Output tab shows the data in three tables: **Reviews**, **Reviewers**, and
**Photos and videos**. You can export it as JSON, CSV, Excel, XML, RSS or JSONL.

### Inputs

| Input | Default | Description |
|---|---|---|
| `startUrls` | required | 1–1,000 Yelp business URLs or aliases |
| `sortBy` | `DATE_DESC` | Newest first, most relevant, oldest first, highest or lowest rated, or Elite reviewers first |
| `ratings` | all | Only collect these star ratings, e.g. `["1", "2"]` for complaints |
| `since` | none | Only reviews posted on or after this date, e.g. `2026-06-01`. Requires newest-first sorting |
| `maxItemsPerTarget` | none | Most reviews to collect from each business |
| `maxTotalItems` | none | Most reviews to collect in the whole run |
| `site` | `US` | Only yelp.com is supported today |

**Rating filter.** Yelp applies `ratings` on its side, so the reviews you filter
out are never downloaded and never charged.

**Limits.** `maxItemsPerTarget` caps each business and `maxTotalItems` caps the
whole run. For example, `maxItemsPerTarget: 100` with `maxTotalItems: 1000`
means no business contributes more than 100 rows and the run never stores more
than 1,000. The Actor also respects the maximum cost per run you set on Apify.

### Incremental runs: only new reviews

Set `since` to the date of your last run and keep `sortBy` at newest first. The
Actor walks each business from its newest review and stops as soon as it
reaches reviews older than your date, so a daily or weekly schedule only
collects and charges for what's new.

One caveat: when a reviewer edits an older review, Yelp keeps its original
date, so an incremental run won't see the edit. To catch edits, run without
`since` every so often and compare by `reviewId`.

### Pricing

You pay only for the reviews saved to your dataset. Proxies, retries and
everything else needed to reach Yelp are included.

### Speed

Up to eight businesses are scraped in parallel. The Actor calls Yelp's own
review feed directly, without a browser, so it runs on little memory. On the
Apify platform, one business's full history of 331 reviews took 16 seconds in
one run and 61 seconds in another; times depend on Yelp's and the proxy's
response latency.

### Run report

The key-value store record `RUN_REPORT` explains what happened to every
business you submitted:

- **outcome**, such as ok, business not found, or network error
- **reviews** and **pages** collected, and Yelp's stated total
- **stopReason**: the history ended, a limit was reached, the `since` date was
  passed, or the run's time limit was near
- **identityConfirmed**: the reviews really belong to the business you asked
  for. Yelp states this on every page and the Actor checks it.

It also lists any inputs that were skipped and why. A business that fails is
reported and the run carries on. The run only fails when none of the businesses
could be scraped.

If a run gets close to its timeout, it stops early, keeps every review already
collected and records `timeLimited: true`. That's better than being cut off
with no report. Raise the timeout or split the list if this happens.

### API example

```bash
curl -X POST \
  "https://api.apify.com/v2/acts/astravalabs~yelp-reviews-scraper/run-sync-get-dataset-items?token=YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"startUrls":["https://www.yelp.com/biz/aji-kiji-san-francisco"],"maxItemsPerTarget":100}'
```

For large batches, start the Actor asynchronously and read the dataset after
the run succeeds.

### Use cases

- **Reputation monitoring:** a scheduled run with `since` gives you a feed of new
  reviews across all your locations.
- **Complaint triage:** use `ratings: ["1", "2"]` to collect only negative
  reviews, along with whether the business replied.
- **Sentiment and topic analysis:** full review text and ratings, ready for an
  LLM or a classifier.
- **Competitor research:** compare ratings, review volume and owner response
  habits across businesses in one run.
- **Reviewer analysis:** see how much weight a review carries from the
  reviewer's review count, Elite status and expert topic.

### Personal data

Yelp reviews are public, but they are written by people. Each row includes the
reviewer's display name, which is usually a first name and last initial, plus
their profile link, profile photo and location. Data like this can count as
personal data under the GDPR, the CCPA and similar laws.

Only collect and keep personal data if you have a legitimate reason to, and
handle it in line with the laws that apply to you. If you only need the review
content, you can drop the `author*` columns from your export. If you are unsure,
consult a lawyer. This Actor collects only what Yelp shows publicly, without
logging in.

### FAQ

**Why did a business return fewer reviews than its Yelp page says?**

Yelp's stated count is often slightly higher than the number of reviews it
actually serves; one business stating 332 served 331. Yelp's
"not currently recommended" reviews aren't in the public feed, so they aren't
collected either. Check `RUN_REPORT` for the stop reason.

**Are non-English reviews included?**

Not yet. Yelp serves each language as a separate list, and the Actor collects
the English one, which on US businesses is the large majority. At the Golden
Gate Bridge, for example, 2,035 of 2,113 reviews are in English; the other 78
(about 4%, mostly German) are not collected.

**Does it work for Yelp sites outside the US?**

Not yet. Only yelp.com is supported, and URLs from other Yelp sites are skipped
and listed in the run report.

**Can I scrape a specific review?**

Each row's `url` is the review's permalink. The Actor itself works per
business, so collect the business and filter by `reviewId`.

> Unofficial. Not affiliated with, endorsed by, or sponsored by Yelp Inc.
> "Yelp" is a trademark of Yelp Inc., used here descriptively.

# Actor input Schema

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

Business page URLs, e.g. https://www.yelp.com/biz/aji-kiji-san-francisco. A bare alias works too.

## `since` (type: `string`):

A date (2026-01-31) or ISO timestamp. The run stops walking a business once it reaches reviews older than this. Requires sorting by newest first. Note: this finds NEW reviews; edits to older reviews keep their original date and will not be picked up, so run without a since date periodically to reconcile.

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

Newest first is required when using a since date.

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

Only collect reviews with these star ratings, e.g. 1 and 2 stars for complaints. Leave empty for all. Yelp filters on its side, so unwanted reviews cost nothing.

## `maxItemsPerTarget` (type: `integer`):

Leave empty for all of them.

## `maxTotalItems` (type: `integer`):

Leave empty for no cap.

## `site` (type: `string`):

Only the US site is supported today.

## Actor input object example

```json
{
  "startUrls": [
    "https://www.yelp.com/biz/aji-kiji-san-francisco"
  ],
  "sortBy": "DATE_DESC",
  "site": "US"
}
```

# Actor output Schema

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

The dataset: one row per review with rating, text, date, reviewer, photos, reactions, edit history and the owner's reply.

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

Per-business outcome, reviews and pages collected, stop reason and request counts.

# 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/aji-kiji-san-francisco"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("astravalabs/yelp-reviews-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/aji-kiji-san-francisco"] }

# Run the Actor and wait for it to finish
run = client.actor("astravalabs/yelp-reviews-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/aji-kiji-san-francisco"
  ]
}' |
apify call astravalabs/yelp-reviews-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,astravalabs/yelp-reviews-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/geHCpWiqcWy56H7xq/builds/Fcb9y4xq0B3igHqGe/openapi.json
