# OpenTable Scraper - Restaurants, Ratings & Diner Data (`abotapi/opentable-reviews-scraper`) Actor

Scrape full OpenTable.com restaurant reviews by keyword, area, or restaurant profile URLs. Collect every available review with text, ratings, diner profile data, tags, photos, restaurant metadata, cuisines, neighborhood, and profile links in structured rows.

- **URL**: https://apify.com/abotapi/opentable-reviews-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** Automation, Developer tools, Other
- **Features:** Limited permissions
- **Stats:** 48 total users, 6 monthly users, 88.9% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 review scrapeds

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

## OpenTable Reviews Scraper: Review Text, Ratings & Diner Data

OpenTable Reviews Scraper turns OpenTable restaurant pages into structured review data. Search by keyword and location, or paste restaurant profile links, and collect every review OpenTable has for each restaurant: full review text, the overall rating plus separate food, service, ambience, value and noise ratings, the dined date, diner details, restaurant replies and review photos, with the parent restaurant's own name, cuisine, neighborhood and rating carried on every row. Export straight to JSON, CSV or Excel, or read the dataset through the API.

### Why This Scraper?

- **Two ways to find restaurants.** Search by keyword and location, or paste OpenTable restaurant links directly. No need to look up restaurant IDs by hand.
- **Full review depth on every row.** Review text plus five per-category ratings, food, service, ambience, value and noise, not just the single overall star score.
- **Deep filtering.** Minimum rating per category, cuisine, neighborhood, VIP reviewers only, photos only, and a dined date window, so you only pay for reviews that match.
- **Restaurant context on every row.** Cuisine, neighborhood, restaurant level rating and total review count are carried on each review, so no extra join is needed. When the restaurant page can be read, each row also carries the restaurant's address, phone, website, opening hours, dining style, dress code, parking, payment options, public transit, coordinates, photo count, review summary and rating distribution.
- **Built to run at scale.** Resume an interrupted pull without paying twice, and cap reviews per restaurant independently from the run wide total.
- **Built for schedules.** Incremental mode returns only new, updated and reappeared reviews on repeat runs, classified per review.
- **Fails loudly, not quietly.** A run that could not reach OpenTable stops with a clear message instead of returning a misleading empty dataset.

### Use Cases

- **Reputation management:** track new reviews and rating trends for your own restaurants as they come in.
- **Competitive research:** compare food, service, ambience and value ratings, and cuisine mix, across nearby restaurants.
- **Market research:** build a dataset of diner sentiment and review volume across neighborhoods or cities.
- **Lead generation:** find highly rated restaurants and their OpenTable profile links for outreach or listing sites.
- **AI and NLP:** feed review text into sentiment analysis, review summarization, or an AI agent answering "where should I eat" questions.

### Data You Get

> Sample shape: values are illustrative placeholders, not from a live record.

| Field | Example |
| --- | --- |
| `reviewId` | `"OT-1000001-2100000001-100000000001"` |
| `reviewText` | `"Great spot for a birthday dinner, the staff were attentive all night."` |
| `ratingOverall` / `ratingFood` / `ratingService` | `5` / `5` / `4` |
| `ratingAmbience` / `ratingValue` | `5` / `4` |
| `ratingNoise` | `"MODERATE"` |
| `dinedDate` / `submittedDateTime` | `"2026-06-09"` / `"2026-06-10T16:02:39Z"` |
| `reviewerName` / `reviewerLocation` | `"Jordan K."` / `"New York City"` |
| `reviewerIsVip` | `false` |
| `reviewerApprovedReviewCount` | `3` |
| `reviewHelpfulUp` / `reviewHelpfulDown` / `reviewHelpfulScore` | `2` / `0` / `2` |
| `restaurantReplyText` | `null` |
| `reviewPhotoUrls` / `reviewPhotoCount` | array of photo URLs / `1` |
| `restaurantName` | `"Sample Sushi House"` |
| `restaurantUrl` | OpenTable link to the restaurant profile |
| `restaurantPrimaryCuisine` / `restaurantCuisines` | `"Japanese"` / `["Japanese", "Sushi"]` |
| `restaurantNeighborhood` | `"Tribeca"` |
| `restaurantRating` / `restaurantTotalReviewCount` | `4.7` / `5397` |
| `restaurantAddressLine1` / `restaurantCity` / `restaurantState` / `restaurantPostalCode` | `"100 Example St"` / `"New York"` / `"NY"` / `"10007"` |
| `restaurantPhone` / `restaurantWebsite` / `restaurantHours` | `"(212) 555-0100"` / link / opening hours text |
| `restaurantDiningStyle` / `restaurantDressCode` / `restaurantParkingInfo` | `"Casual Elegant"` / `"Business Casual"` / `"Public Lot"` |
| `sourceMode` / `sourceQuery` | `"search"` / `"sushi"` |
| `scrapedAt` | timestamp of when this row was scraped |

**Restaurant detail columns are best effort.** The address, phone, website, hours, dining style, dress code, description, parking, payment options, public transit, cross street, executive chef, amenities (`restaurantFeatures`, `restaurantAdditionalDetails`), popular dishes, delivery partners, photo count, coordinates, country, metro and neighborhood ids, review summary, rating basis, rating distribution, review categories, awards, and, on the restaurants whose page carries them, accessibility details, gender-neutral restroom, grocery store and call-for-delivery flags, currency code, counts of diners who recommend and do not recommend, affiliated-restaurants flag and network listing status (`restaurant*` columns, see the Restaurant details view) come from the restaurant's public OpenTable page. That page loads on only some connections, so the actor tries a few times per restaurant; when it still cannot be read those columns are `null` on that restaurant's rows, and the review rows are never held back or billed differently. After several restaurants in a row come back without details, the run stops trying for the rest of that run.

Every row also carries the restaurant's internal id, metro area and broader region, price band and currency symbol, and its profile photo; the review's own type flag and exact dined timestamp; the reviewer's initials, profile color and photo when OpenTable provides them; and the source URL when the row came from URL mode. Incremental mode (below) adds `changeType`, `changedFields`, `firstSeenAt` and `lastSeenAt` to every row.

### How to Use

1. Pick a mode: **Keyword search** (restaurants around a location) or **Restaurant URLs** (paste OpenTable profile links).
2. For keyword search, add search terms and a location, or set exact latitude and longitude; for URL mode, paste one or more restaurant profile links.
3. Optionally narrow results with the rating, cuisine, neighborhood, VIP, photo, and dined date filters, and set **Max items** to control run size and cost.
4. Click **Start**, then download the dataset as JSON, CSV or Excel, or read it through the API.

**Keyword search:**

```json
{
  "mode": "search",
  "keywords": ["sushi", "steakhouse"],
  "location": "New York",
  "maxItems": 20
}
```

**Keyword search, filtered to highly rated reviews with photos:**

```json
{
  "mode": "search",
  "keywords": ["rooftop"],
  "location": "Miami",
  "minOverallRating": 4,
  "requireReviewPhotos": true,
  "maxItems": 50
}
```

**Restaurant URLs:**

```json
{
  "mode": "url",
  "startUrls": [
    { "url": "https://www.opentable.com/r/yakiniku-shodai-san-francisco" }
  ],
  "maxReviewsPerRestaurant": 200
}
```

**Supported URL shapes (url mode):**

| Shape | Example | Behaviour |
|---|---|---|
| Restaurant page | `https://www.opentable.com/r/yakiniku-shodai-san-francisco` | That restaurant's reviews. Matched by profile slug; an unmatched slug logs a warning and is skipped, never replaced by another restaurant. |
| Search page | `https://www.opentable.com/s?term=sushi&metroId=4&covers=2` | Search by `term`; `lat`/`lng` parameters set the location; other parameters such as `covers` are ignored. A `metroId=4` link is mapped to San Francisco; any other `metroId` alone cannot be mapped, so a warning is logged and the latitude/longitude input (default New York) is used. |
| City listings page | `https://www.opentable.com/metro/san-francisco-restaurants` | Restaurants around that city. The older `/<city>-restaurant-listings` form (for example `/new-york-restaurant-listings`) is still accepted, although OpenTable itself no longer serves that address. |
| Numeric restaurant id | `https://www.opentable.com/restaurant/profile/4528` or `...restref/client?rid=4528` | Reviews for that restaurant id. Name, link, cuisine and the price band are filled only when the same run also meets that restaurant through a restaurant page, search or city source; the address, phone, hours and the other page details are read from the id link itself when the page loads. |

Localized hosts (for example opentable.co.uk) and any other link shape are skipped with a warning in the run log.

**Recurring monitoring of the same search:**

```json
{
  "mode": "search",
  "keywords": ["steakhouse"],
  "location": "Chicago",
  "incrementalMode": true,
  "stateKey": "chicago-steakhouses"
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/opentable-reviews-scraper").call(run_input={"mode": "search", "keywords": ["sushi"], "location": "New York"})
for review in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(review["restaurantName"], review["ratingOverall"], review["reviewText"])
```

JavaScript:

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/opentable-reviews-scraper').call({ mode: 'search', keywords: ['sushi'], location: 'New York' });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the **Integrations** tab.

#### Output views and run summary

The Output tab has four views over the same review rows: **Overview** (everything), **Restaurant details** (address, phone, website, hours and the other restaurant page columns), **Ratings & restaurant** (the five per-category ratings next to the restaurant's own rating, review count, cuisine and price band) and **Changes (incremental)** (change type, changed fields and first/last seen timestamps, filled when incremental mode is on). When a run covers more than one keyword or URL, a run summary is saved to the key-value store under `OUTPUT` with the rows and status per source.

#### Resume and recurring updates

- **Resume** (`resumeFromRunId`) continues one interrupted run: paste its run or dataset ID and the actor skips every review it already collected there, so you don't pay twice.
- **Incremental mode** (`incrementalMode`) is for recurring monitoring of the same search or URLs. Each review is classified `NEW`, `UPDATED` (with `changedFields`), `UNCHANGED` (suppressed and not billed unless `emitUnchanged` is on), `REAPPEARED`, or `EXPIRED` (only after a run that scanned every tracked restaurant in full, and only with `emitExpired`). `stateKey` names or shares the stored state; leave it empty to derive one automatically from the search or URLs and the active filters. **What counts as a change.** A review is `UPDATED` when its own content changes (review text, any rating, dined or submitted date, restaurant reply, reviewer details, photo count, restaurant name, cuisine or location); `changedFields` names exactly what moved. These never count, though they stay in every row: `scrapedAt`, the helpful counters (`reviewHelpfulUp`, `reviewHelpfulDown`, `reviewHelpfulScore`), the restaurant counters (`restaurantRating`, `restaurantTotalReviewCount`) the photo links (`restaurantPhotoUrl`, `reviewerPhotoUrl`, `reviewPhotoUrls`) and every restaurant detail column from the restaurant page (address, phone, hours and the rest), which can be empty on one run and filled on the next. Use `ignoreFieldsForChanges` to exclude more. Reviews saved by an earlier version are compared with the same rules, so upgrading does not mark everything UPDATED. Combining Resume with Incremental mode is only allowed before that state key has saved data; once it has, remove Resume or pick a different State key.

#### How Max restaurants and Max items interact (keyword search)

Keyword search first collects up to **Max restaurants per keyword** restaurants around your coordinates, then pulls reviews from each one until **Max items** is reached. A keyword that returns exactly the restaurant cap may have more restaurants available; raise Max restaurants per keyword to check. Max items is the hard stop on total review rows across every keyword and restaurant in the run.

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `search` | `search` (keyword and location discovery) or `url` (paste restaurant profile links). |
| `keywords` | array | (none) | Restaurant, cuisine, or dining terms to search (search mode). An OpenTable restaurant link (`/r/<slug>`) pasted here is read as a restaurant URL. |
| `location` | string | `New York` | City, neighborhood, or address to search around (search mode); resolved to coordinates automatically. |
| `latitude` | number | (none) | Optional. Exact latitude; together with Longitude it overrides Location. |
| `longitude` | number | (none) | Optional. Exact longitude; together with Latitude it overrides Location. |
| `startUrls` | array | 3 example URLs (one per shape below) | OpenTable links to scrape (url mode). Behaviour change: omitting `startUrls` in url mode now runs the three example URLs instead of nothing. |
| `maxItems` | integer | `20` | Maximum review rows saved across the whole run. The run can return fewer when OpenTable has fewer matching reviews. |
| `maxReviewsPerRestaurant` | integer | `1000000` | Cap on reviews collected per restaurant. The default is effectively no per-restaurant limit, so collection is bounded only by Max items (default 20). |
| `maxRestaurants` | integer | `50` | Keyword search only. How many restaurants around the coordinates to pull reviews from, per keyword. |
| `reviewSort` | string | `highestRated` | Order reviews are returned in, per restaurant: `highestRated`, `newest`, or `lowestRated`. |
| `minOverallRating` | number | (none) | Only keep reviews with at least this overall rating. |
| `minFoodRating` | number | (none) | Only keep reviews with at least this food rating. |
| `minServiceRating` | number | (none) | Only keep reviews with at least this service rating. |
| `minAmbienceRating` | number | (none) | Only keep reviews with at least this ambience rating. |
| `minValueRating` | number | (none) | Only keep reviews with at least this value rating. |
| `cuisines` | array | (none) | Only keep restaurants whose primary or secondary cuisine matches at least one value. |
| `neighborhoods` | array | (none) | Only keep restaurants whose neighborhood matches at least one value. |
| `fromDinedDate` | string | (none) | Only keep reviews with a dined date on or after this YYYY-MM-DD date. |
| `toDinedDate` | string | (none) | Only keep reviews with a dined date on or before this YYYY-MM-DD date. |
| `dateTimezone` | string | (none) | IANA timezone such as `Australia/Melbourne`. OpenTable's page shows each dining time in the viewer's own timezone, so set yours to make `dinedDate` and the Dined from / to window match your screen. Empty = restaurant-local date. Setting it starts a new baseline for recurring (incremental) runs. |
| `fromReviewDate` | string | (none) | Only keep reviews posted (submitted) on or after this YYYY-MM-DD date. |
| `toReviewDate` | string | (none) | Only keep reviews posted (submitted) on or before this YYYY-MM-DD date. |
| `vipOnly` | boolean | `false` | Only keep reviews where OpenTable marks the reviewer as VIP. |
| `requireReviewPhotos` | boolean | `false` | Only keep reviews that include diner photos. |
| `resumeFromRunId` | string | (none) | Continue one interrupted run: paste its run or dataset ID and already collected reviews are skipped and not re-charged. |
| `incrementalMode` | boolean | `false` | Turn on for recurring monitoring of the same search or URLs. Classifies each review as NEW, UPDATED, UNCHANGED, REAPPEARED, or EXPIRED against the previous run for this state. |
| `emitUnchanged` | boolean | `false` | Also return, and bill, reviews unchanged since the last run (incremental mode only). |
| `emitExpired` | boolean | `false` | Also return, and bill, reviews that were tracked before but are no longer found (incremental mode only). Only produced when a run fully scans every tracked restaurant or search. |
| `stateKey` | string | (none) | Name or share an incremental mode monitoring state. Leave empty to derive one automatically from the search or URLs and the active filters. |
| `ignoreFieldsForChanges` | array | (none) | Incremental mode only. Changes to only these fields do not mark a review UPDATED; the fields stay in the output. Plain names or dotted paths for nested fields. Added on top of the built-in ignore list. |
| `proxy` | object | Apify Proxy | Connection routing for the run. |
| `mcpConnectors` | array | (none) | Connectors to send the saved review rows to after the run (Notion, Linear, Airtable, Apify). Best-effort; never changes the dataset. |
| `notionParentPageUrl` | string | (none) | Notion page under which one page per review is created. Needed only for Notion export. |
| `maxNotifyListings` | integer | `50` | Cap on rows sent to each connector per run. Does not affect the dataset. |

### Send results into your apps (MCP connectors)

Optionally pipe the review rows saved in this run into apps you have authorized under Apify > Settings > API & Integrations. Select them in `mcpConnectors`; for Notion also set `notionParentPageUrl`, and cap the rows sent with `maxNotifyListings` (default 50). The export is best-effort: a failure logs a warning and never changes the dataset or billing.

### Output Example

> Sample shape: values are illustrative placeholders, not from a live record.

```json
{
  "reviewId": "OT-1000001-2100000001-100000000001",
  "reviewText": "Great spot for a birthday dinner, the staff were attentive all night and the sushi was excellent.",
  "ratingOverall": 5,
  "ratingFood": 5,
  "ratingService": 4,
  "ratingAmbience": 5,
  "ratingValue": 4,
  "ratingNoise": "MODERATE",
  "reviewType": "OPENTABLEVERIFIED",
  "submittedDateTime": "2026-06-10T16:02:39Z",
  "dinedDateTime": "2026-06-09T18:45:00-04:00",
  "dinedDate": "2026-06-09",
  "reviewHelpfulUp": 2,
  "reviewHelpfulDown": 0,
  "reviewHelpfulScore": 2,
  "restaurantReplyText": null,
  "reviewerName": "Jordan K.",
  "reviewerInitials": "JK",
  "reviewerIsVip": false,
  "reviewerLocation": "New York City",
  "reviewerApprovedReviewCount": 3,
  "reviewerProfileColor": "#3E7C59",
  "reviewerPhotoUrl": null,
  "reviewPhotoUrls": ["https://www.opentable.com/example-assets/photos/sample-1.jpg"],
  "reviewPhotoCount": 1,
  "restaurantId": 1000001,
  "restaurantName": "Sample Sushi House",
  "restaurantUrl": "https://www.opentable.com/r/sample-sushi-house-new-york",
  "restaurantMetro": "New York City",
  "restaurantMacro": "Northeast",
  "restaurantNeighborhood": "Tribeca",
  "restaurantPrimaryCuisine": "Japanese",
  "restaurantCuisines": ["Japanese", "Sushi"],
  "restaurantPriceBand": 3,
  "restaurantCurrencySymbol": "$",
  "restaurantRating": 4.7,
  "restaurantPhotoUrl": "https://www.opentable.com/example-assets/restaurants/sample-profile.jpg",
  "restaurantTotalReviewCount": 5397,
  "restaurantAddressLine1": "100 Example St",
  "restaurantCity": "New York",
  "restaurantState": "NY",
  "restaurantPostalCode": "10007",
  "restaurantCountryCode": "US",
  "restaurantPhone": "(212) 555-0100",
  "restaurantWebsite": "https://www.example.com/sample-sushi-house",
  "restaurantDiningStyle": "Casual Elegant",
  "restaurantDressCode": "Business Casual",
  "restaurantPaymentOptions": ["AMEX", "Visa"],
  "sourceMode": "search",
  "sourceQuery": "sushi",
  "sourceUrl": null,
  "scrapedAt": "2026-06-15T09:12:00.000000+00:00"
}
```

### Plan Requirement

OpenTable guards its pages with a strict bot filter, and a plain datacenter connection is not reliable: it is often refused outright or served an empty challenge page instead of content. A paid Apify plan with proxy access is recommended, and the **residential** group under Connection gives the most headroom. The actor rotates connections automatically when one is refused.

- **Free plan:** Apify free plans include no proxy and no residential access. The run logs "The selected connection is unavailable to this run. Apify free plans do not include residential proxies. Continuing on a backup connection; some pages may still be unavailable." and continues on a backup connection, logging "Served over the backup connection (the default connection was unavailable)." when it works. Expect lower success rates than on a paid plan.
- **Failure messages:** if your account has no proxy access, the run stops with "The proxy connection was refused for this account, so no page could be read. Your Apify plan does not include proxy access; a paid plan is required". If residential is unavailable and the backup connection could not load the pages, it stops with "No results: residential proxies are unavailable on this run (Apify free plans do not include them), and the backup connection could not load the requested OpenTable pages." If no page could be loaded at all, it stops with "Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search."
- **Your own proxy:** you can supply your own proxy URLs instead.

### FAQ

#### How much does it cost?

You pay per review row returned; runs that find no matching reviews are not charged for that event. Review rates fall with your Apify plan tier. There is also an `apify-actor-start` event, billed per GB of memory at the start of each run. The **Pricing** tab always shows the current rates. Use **Max items** to cap the cost of any run. **Max items** defaults to 20 and is the hard ceiling for the whole run: **Max reviews per restaurant** can only lower it, so a run never saves (or bills) more review rows than Max items.

#### Is it legal to scrape OpenTable reviews?

This actor collects only publicly available review data. You are responsible for how you use it: follow OpenTable's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Review text and ratings belong to the diners and restaurants that posted them.

#### Can I get only new reviews on a schedule?

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental mode**. Each run then classifies every review as new, updated, unchanged, reappeared, or expired against the previous run for that search, and unchanged reviews are not billed unless you turn that on.

#### Why do I get one row with `"error": true` instead of an empty dataset?

When a run finds zero matching reviews, whether from an unmatched search, an unresolved restaurant URL, or filters that exclude everything, the actor pushes a single row shaped like `{"error": true, "code": "ZERO_RESULTS", "message": "..."}` instead of leaving the dataset empty, so "nothing matched" is never confused with a run that silently produced nothing. Try broader keywords, nearby coordinates, fewer filters, or different restaurant URLs.

#### Why did my run fail instead of returning results?

A run fails, rather than returning an empty or ZERO_RESULTS dataset, when the actor could not establish a working connection to OpenTable at all, when it lost that connection partway through, could not recover, and had not saved any reviews yet (if some reviews were already saved, the run finishes with those partial results), when `resumeFromRunId` points at a run or dataset ID it cannot read, or when Incremental mode already has saved state for a search and `resumeFromRunId` is set at the same time. Each of these stops the run with a clear status message describing what to check or retry.

#### Can I use it with AI agents or MCP?

Yes. Call it from any Apify integration, or from an AI agent through Apify's MCP server.

### 🔗 More scrapers you might like

Pair this actor with these related scrapers from the same team:

<table>
<tr><td>🛒 <a href="https://apify.com/abotapi/zomato-scraper"><b>Zomato Scraper</b></a><br>Scrape restaurants and reviews from zomato.com. Get names, cuisines, ratings and votes...</td><td>🛒 <a href="https://apify.com/abotapi/grabfood-restaurants-scraper"><b>GrabFood Restaurant Scraper</b></a><br>Scrape GrabFood restaurants across Southeast Asia, including SG, MY, TH, VN, PH, ID, KH...</td></tr>
<tr><td>🧩 <a href="https://apify.com/abotapi/resy-restaurant-availability-scraper"><b>Resy Scraper</b></a><br>Scrape Resy restaurants and live reservation availability by city or URL. Get bookable...</td><td>🛒 <a href="https://apify.com/abotapi/wolt-restaurants-scraper"><b>Wolt.com</b></a><br>From $0.8/1K. Scrape Wolt restaurants and full menus at the city scale. Extract 60+...</td></tr>
<tr><td>🛒 <a href="https://apify.com/abotapi/doordash-scraper"><b>DoorDash Scraper</b></a><br>Extract structured doordash.com data at scale. Search by keyword or paste store URLs to...</td><td>🏠 <a href="https://apify.com/abotapi/apartments-com-scraper"><b>Apartments.com Scraper</b></a><br>From $1.50/1K. Scrape Apartments.com rentals by city, ZIP, neighborhood, address or URL...</td></tr>
</table>

👉 [Browse all abotapi scrapers](https://apify.com/abotapi)

### 💬 Support

- 🐞 **Found a bug or a missing field?** Open a ticket on the [Issues tab](https://apify.com/abotapi/opentable-reviews-scraper/issues/open). We usually reply within hours.
- 🛠️ **Need another site, extra fields or a private build?** Email <contact@abotapi.com>.
- ⭐ **Enjoying it?** A quick review on the actor page helps other users find it.

# Actor input Schema

## `mode` (type: `string`):

Search mode discovers restaurants around the selected coordinates and collects their full review history. URL mode takes pasted restaurant profile links and collects every review for each one.

## `keywords` (type: `array`):

Restaurant, cuisine, or dining terms to search. Examples: sushi, steakhouse, rooftop, vegan. An OpenTable restaurant link (/r/<slug>) pasted here is treated as a restaurant URL.

## `location` (type: `string`):

City, neighborhood, or address to search around (keyword search mode). For example: New York, Chelsea Manhattan, or 90210. The actor resolves this to map coordinates automatically.

## `latitude` (type: `number`):

Optional. Exact latitude to search around. When both latitude and longitude are set they override Location.

## `longitude` (type: `number`):

Optional. Exact longitude to search around. When both latitude and longitude are set they override Location.

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

Paste OpenTable links: a restaurant page (/r/<slug>), a search page (/s?term=...), a city listings page (/metro/<city>-restaurants or /<city>-restaurant-listings) or a numeric restaurant id link (/restaurant/profile/<id>). Other or localized links are skipped with a warning.

## `maxItems` (type: `integer`):

Maximum review rows to save across the whole run. The run can return fewer rows when OpenTable has fewer matching reviews.

## `maxReviewsPerRestaurant` (type: `integer`):

Optional cap on how many reviews to collect from each restaurant. The default (1000000) is effectively no per-restaurant limit, so collection is bounded only by Max items.

## `maxRestaurants` (type: `integer`):

Keyword search only. How many restaurants around the coordinates to collect reviews from per keyword.

## `reviewSort` (type: `string`):

Order of reviews returned for each restaurant.

## `minOverallRating` (type: `number`):

Only keep reviews with at least this overall review rating.

## `minFoodRating` (type: `number`):

Only keep reviews with at least this food rating.

## `minServiceRating` (type: `number`):

Only keep reviews with at least this service rating.

## `minAmbienceRating` (type: `number`):

Only keep reviews with at least this ambience rating.

## `minValueRating` (type: `number`):

Only keep reviews with at least this value rating.

## `cuisines` (type: `array`):

Only keep restaurants whose primary or secondary cuisine contains at least one value.

## `neighborhoods` (type: `array`):

Only keep restaurants whose neighborhood contains at least one value.

## `fromDinedDate` (type: `string`):

Only keep reviews with a dined date on or after this YYYY-MM-DD date.

## `toDinedDate` (type: `string`):

Only keep reviews with a dined date on or before this YYYY-MM-DD date.

## `dateTimezone` (type: `string`):

Optional IANA timezone (e.g. Australia/Melbourne, America/New_York). OpenTable's page prints each dining time in the viewer's own browser timezone, so a viewer in a different timezone can see a different day than the restaurant's local one. Set your timezone to make the dined date and the Dined from / to filters match what you see on OpenTable. Empty = the restaurant's local date. Setting it starts a new baseline for recurring (incremental) runs.

## `fromReviewDate` (type: `string`):

Only keep reviews posted (submitted) on or after this YYYY-MM-DD date. Independent of the dined date.

## `toReviewDate` (type: `string`):

Only keep reviews posted (submitted) on or before this YYYY-MM-DD date.

## `vipOnly` (type: `boolean`):

Only keep reviews where OpenTable marks the reviewer as VIP.

## `requireReviewPhotos` (type: `boolean`):

Only keep reviews that include diner photos.

## `proxy` (type: `object`):

Connection routing for the run. The default is the right one for OpenTable. If the selected connection is unavailable to your plan, the run continues on a backup connection automatically; you can also supply your own proxy URLs.

## `resumeFromRunId` (type: `string`):

Paste a previous run ID or dataset ID to continue a large review pull without returning or charging for reviews already collected there. Use this after an interrupted run, or when continuing a large restaurant's review history in another run. For recurring monitoring of the same search or restaurants, use Incremental mode below instead.

## `incrementalMode` (type: `boolean`):

Turn this on for daily or recurring monitoring. The first run returns all matching reviews as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED reviews. A review is UPDATED when its own content changes: review text, any rating, dined or submitted date, reply text, reviewer details, photo count, or the restaurant name, cuisine or location. Changes to the helpful counters, the restaurant rating and total review count, and photo links never count (they still appear in the output); add more with "Ignore fields when detecting changes". Turn on "Emit unchanged" or "Emit expired" only when you also want those reviews returned (and billed). State is kept separately for each search/URL and filter setup; use State key when you want to name or deliberately share a monitoring campaign. To continue one specific interrupted run instead, use Resume from a previous run above.

## `emitUnchanged` (type: `boolean`):

Off by default. Turn on to also return reviews that have not changed since the last run, marked UNCHANGED. This returns — and bills — extra rows you already have, so leave it off unless you specifically want the full snapshot every run.

## `emitExpired` (type: `boolean`):

Off by default. Turn on to also return reviews that were present in a previous run but are no longer found, marked EXPIRED. Only produced once a run has fully scanned the tracked restaurants — not when Max items capped it, when Resume was used, or when a keyword search may have been truncated. This returns — and bills — extra synthetic rows, so leave it off unless you need expiry tracking.

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

Optional. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently-configured runs. Leave empty to let the actor derive a key automatically from the search/URL and filter settings — different searches then never mix state with each other.

## `ignoreFieldsForChanges` (type: `array`):

Incremental mode only: changes to ONLY these fields do not mark a review UPDATED (the fields still appear in the output). Add your own field names, or a dotted path for a nested field. Already ignored by default: scrapedAt, the helpful counters (reviewHelpfulUp, reviewHelpfulDown, reviewHelpfulScore), the restaurant counters (restaurantRating, restaurantTotalReviewCount) and the photo links (restaurantPhotoUrl, reviewerPhotoUrl, reviewPhotoUrls).

## `mcpConnectors` (type: `array`):

Optionally send scraped review rows into connected apps via Model Context Protocol connectors. Authorize a connector under Apify > Settings > API & Integrations, then select it here. Supported: Notion, Linear, Airtable, and Apify.

## `notionParentPageUrl` (type: `string`):

URL or id of the Notion page under which review pages are created. Required for Notion export; ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on how many review rows are written to each selected connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "search",
  "keywords": [
    "sushi"
  ],
  "location": "New York",
  "startUrls": [
    {
      "url": "https://www.opentable.com/r/yakiniku-shodai-san-francisco"
    },
    {
      "url": "https://www.opentable.com/s?term=sushi&metroId=4&covers=2"
    },
    {
      "url": "https://www.opentable.com/metro/san-francisco-restaurants"
    }
  ],
  "maxItems": 20,
  "maxReviewsPerRestaurant": 1000000,
  "maxRestaurants": 50,
  "reviewSort": "highestRated",
  "vipOnly": false,
  "requireReviewPhotos": false,
  "proxy": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `changes` (type: `string`):

No description

## `ratings` (type: `string`):

No description

## `restaurant` (type: `string`):

No description

# 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 = {
    "mode": "search",
    "keywords": [
        "sushi"
    ],
    "location": "New York",
    "startUrls": [
        {
            "url": "https://www.opentable.com/r/yakiniku-shodai-san-francisco"
        },
        {
            "url": "https://www.opentable.com/s?term=sushi&metroId=4&covers=2"
        },
        {
            "url": "https://www.opentable.com/metro/san-francisco-restaurants"
        }
    ],
    "proxy": {
        "useApifyProxy": true
    },
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "ignoreFieldsForChanges": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/opentable-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 = {
    "mode": "search",
    "keywords": ["sushi"],
    "location": "New York",
    "startUrls": [
        { "url": "https://www.opentable.com/r/yakiniku-shodai-san-francisco" },
        { "url": "https://www.opentable.com/s?term=sushi&metroId=4&covers=2" },
        { "url": "https://www.opentable.com/metro/san-francisco-restaurants" },
    ],
    "proxy": { "useApifyProxy": True },
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "ignoreFieldsForChanges": [],
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/opentable-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 '{
  "mode": "search",
  "keywords": [
    "sushi"
  ],
  "location": "New York",
  "startUrls": [
    {
      "url": "https://www.opentable.com/r/yakiniku-shodai-san-francisco"
    },
    {
      "url": "https://www.opentable.com/s?term=sushi&metroId=4&covers=2"
    },
    {
      "url": "https://www.opentable.com/metro/san-francisco-restaurants"
    }
  ],
  "proxy": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": []
}' |
apify call abotapi/opentable-reviews-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/opentable-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/Kb2o1nGdqfFWhkafQ/builds/vcTIPoRhylX5euJz5/openapi.json
