# OpenTable Restaurant Scraper – Reviews, Ratings & Phone Numbers (`rl1987/opentable-api-scraper`) Actor

Scrape OpenTable restaurants in any city, by area, address or lat-lng, with an optional cuisine keyword. Each row: name, profile URL, cuisines, price band, rating, review count, neighbourhood, address, coordinates. Opt in for phone, website, hours, menu, popular dishes and diner reviews.

- **URL**: https://apify.com/rl1987/opentable-api-scraper.md
- **Developed by:** [R.L.](https://apify.com/rl1987) (community)
- **Categories:** Travel, Lead generation, E-commerce
- **Features:** Limited permissions
- **Stats:** 24 total users, 13 monthly users, 99.8% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.27 / 1,000 restaurants

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 Restaurant Scraper — Reviews, Ratings & Phone Numbers

**Scrape restaurants from OpenTable (opentable.com) in any city OpenTable covers.** Search by city, area or address — or by exact coordinates — with an optional cuisine keyword, and export clean records: name, profile URL, cuisines, price band, star rating, review count, neighbourhood, full address and coordinates. Switch on the optional deep scrape for **phone numbers**, website, opening hours, **full menus** (every section, item, description and price), **popular dishes**, **reservation availability** (open tables by date and party size) and full **diner reviews**. Or skip the search: give it the restaurants you care about by URL, ID or name and pick a single-purpose mode, so a scheduled run tracking menu changes fetches nothing else. It reads OpenTable's own mobile API directly, so it's fast and complete — no flaky HTML parsing, no headless browser. Export to **JSON, CSV, Excel, or an API**.

***

### Why use this OpenTable scraper?

- **No code required** — set your input, click **Start**, download your data.
- **Fast & accurate** — reads OpenTable's own mobile API and returns structured JSON, not scraped HTML.
- **Rich, structured data** — 25 fields from search alone, 41 with the deep scrape on.
- **Full pagination** — 30 restaurants per page, collected up to your `maxItems` cap, with cross-page duplicates removed.
- **Transparent pay-per-event pricing** — you are charged for what a run delivers; the **Pricing** tab lists the events and their current prices.
- **Export anywhere** — JSON, CSV, Excel, XML, or pull it live via the Apify API.

***

### What can you do with OpenTable data?

- **Restaurant market & competitor research** — compare cuisines, price bands and ratings across a city or a neighbourhood.
- **Demand & popularity tracking** — `resosInLastDay` (bookings in the last 24 hours) and `firstNextAvailableTime` show how booked a restaurant is, sampled over time.
- **Review mining** — per-category diner scores for food, service, ambience, value and noise, with the review text.
- **Lead generation** — restaurant phone numbers, websites and addresses for a whole metro.
- **Data science** — clean, flat datasets for pricing, rating and cuisine analysis.

***

### What data does the OpenTable scraper extract?

Each row is one restaurant. From search alone you get: `id`, `name`, `url`, `cuisines`, `priceBand`, `priceBandId`, `rating`, `reviewCount`, `neighborhood`, `neighborhoodId`, `metroId`, `address`, `city`, `state`, `postalCode`, `country`, `latitude`, `longitude`, `images`, `description`, `features`, `firstNextAvailableTime`, `reviews`, `reviewsFetched`. The deep-scrape options add 16 more. See the full **Data table** below.

***

### How do I scrape OpenTable restaurants? (3 steps)

1. **Set your search area** — a `location` string (city, area or address), or `lat` + `lon`. Add `q` for a cuisine or keyword.
2. **Pick options** — `maxItems` to cap the run, and the two deep-scrape toggles if you need phones or reviews.
3. **Run & export** — click **Start**, then download the dataset as JSON/CSV/Excel or fetch it via the API.

***

### Can I get phone numbers, menus and diner reviews?

Yes — all four deep-scrape options are **off by default** and each is billed as its own
pay-per-event surcharge, so a plain search run stays cheap. Current prices are on the
[Pricing tab](https://apify.com/rl1987/opentable-api-scraper/pricing).

| Option | Adds |
|---|---|
| **Include restaurant details** | **Phone number** (raw digits + formatted), website, opening hours, dining style, dress code, parking, payment options, menu link, popular dishes, max party size, booking window, currency, takeout flag, gender-neutral restroom flag, bookings in the last 24h |
| **Include full menu** | Every OpenTable-hosted menu: sections, dishes, prices, descriptions and dish photos, with the menu provider's attribution preserved |
| **Include diner reviews** | Review text, author and their home metro, posted/reservation dates, VIP and loyalty tier, and per-category ratings — overall, food, service, ambience, value, noise |
| **Include availability** | Bookable time slots for a date and party size, or OpenTable's own reason there are none |

Reviews are paged automatically to `reviewsPerRestaurant` and can be ordered by
**newest**, **highest rated**, **lowest rated** or **recommended**. Enrichment runs in
parallel (`detailConcurrency`, default 8) — 30 restaurants with both options take roughly
25 seconds rather than several minutes.

#### Details need a US proxy

OpenTable geo-gates the restaurant-profile route. It does not return an error from a non-US
IP — it simply **hangs until the request times out**. Measured on the Apify platform, same
input, 5 restaurants:

| Proxy | Phones returned | Run time |
|---|---|---|
| Apify Proxy (default datacenter) | 3 / 5 | 133 s |
| **Apify Proxy — RESIDENTIAL, country US** | **5 / 5** | **24 s** |

**Residential US is therefore the default** — details work out of the box, no configuration
needed. It is not a speed penalty: a 100-record search-only run took 18.5 s on residential
vs 16.1 s on datacenter, and used only 0.00012 GB of residential transfer (~860k records per
10 GB).

Search and reviews are not geo-gated, so a datacenter group works for search-only jobs if
you prefer one; keep residential whenever details, menus or availability are on, because
those routes are US-gated and hang rather than erroring from elsewhere. Either way the
proxy transfer is not billed to you on top of the event charges.

***

### Modes

One run does one job. `mode` defaults to `search`, so anything that worked before still works unchanged.

| mode | needs | returns |
|---|---|---|
| `search` *(default)* | `location` or `lat`+`lon` | restaurants by city, area or coordinates — plus whichever `include*` extras you switch on |
| `restaurants` | `restaurantUrls` / `restaurantNames` | phone, hours, website, popular dishes |
| `menus` | `restaurantUrls` / `restaurantNames` | every menu, section, item and price |
| `reviews` | `restaurantUrls` / `restaurantNames` | diner reviews with category ratings |
| `availability` | `restaurantUrls` / `restaurantNames` | open tables by date and party size |

An enrichment mode fetches **only** its own data, so a monitoring run that wants menus does not pay for reviews — it sets its own `include*` flag and clears the other three. It ignores the search inputs (`location`, `q`, `sort`, the filters) and logs which ones it dropped, rather than leaving you to wonder why `q` did nothing.

In `search` mode the `include*` flags are additive and behave exactly as they always have.

```json
{ "mode": "menus",
  "restaurantUrls": ["1230112", "https://www.opentable.com/restaurant/profile/42265"] }
```

```json
{ "mode": "availability",
  "restaurantNames": ["Brooklyn Chop House Times Square, New York"],
  "daysAhead": 7, "time": "19:00", "partySize": 2 }
```

### What can I filter on?

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `search` | What the run does: `search`, or `restaurants` / `menus` / `reviews` / `availability` to fetch one kind of data for restaurants you list. See [Modes](#modes). |
| `restaurantUrls` | array | – | Restaurants to fetch, for every mode except `search`: a numeric ID (`42265`), a profile URL, or any opentable.com restaurant URL — slug URLs are resolved automatically. |
| `restaurantNames` | array | – | Restaurants by name, one per line as `Name, City` — e.g. `Brooklyn Chop House Times Square, New York`. An ambiguous name is reported with the near-misses rather than guessed. |
| `location` | string | `New York, NY` | City, area, or address to search around (e.g. "New York, NY", "San Francisco"). Resolved to coordinates through OpenTable's own autocomplete at run time. |
| `q` | string | – | Optional cuisine or keyword (e.g. "sushi", "Indian"). |
| `lat` | number | – | Optional latitude override; skips the location lookup. |
| `lon` | number | – | Optional longitude override; skips the location lookup. |
| `maxItems` | integer | `100` | Maximum number of restaurants to return (0 = all pages). |
| `includeDetails` | boolean | `false` | Fetch each restaurant's full profile: <b>phone number</b>, website, opening hours, dining style, dress code, parking, payment options, menu link and popular dishes. One extra request per restaurant, billed as its own event. <b>Requires a US proxy</b> — this route is geo-gated and simply hangs from non-US IPs. |
| `includeReviews` | boolean | `false` | Fetch diner reviews per restaurant — text, author, dates and the per-category ratings (food, service, ambience, value, noise). Billed per review returned. |
| `includeMenu` | boolean | `false` | Fetch the complete OpenTable-hosted menu per restaurant: every menu, section, item, description, price and size variation, plus the menu's currency and data provider. One extra request per restaurant. Separate from the `menuUrl` field, which links to the menu on the restaurant's <i>own</i> website — that one comes from the profile, so it needs `includeDetails`. A restaurant can have either source, both or neither; enable both options to capture both. Not currently billed as a separate event. |
| `includeAvailability` | boolean | `false` | Fetch bookable reservation slots per restaurant: time, seating type, dining area, loyalty points, and whether a card is required (hold vs deposit). One extra request per restaurant. **Needs a US proxy** — like the profile route, this hangs from non-US IPs rather than erroring. |
| `date` | string | tomorrow | Reservation date, `YYYY-MM-DD`. |
| `daysAhead` | integer | – | Check this many days from the run date (`0` today, `7` a week out). Use instead of `date` for scheduled monitoring — a relative target stays correct every run. Setting both is rejected. |
| `time` | string | `19:00` | Reservation time, `HH:MM` 24-hour. |
| `partySize` | integer | `2` | Number of diners. |
| `cuisines` | array | – | Filter by cuisine, applied upstream by OpenTable. Display names (`Italian`, `Steakhouse`) are resolved to OpenTable's ids automatically; raw UUIDs also work. An unknown name is reported and ignored. |
| `neighborhoods` | array | – | Filter by neighbourhood (`Chelsea`, `Astoria`). Names resolved to ids (`n_hood:195`, `c_hood:292`), which you may pass directly. |
| `priceBands` | array | – | OpenTable price bands: `2` = $30 and under, `3` = $31 to $50, `4` = $50 and over. (`1` returns nothing.) |
| `awardTags` | array | – | Award holders, e.g. `MICHELIN`, `MICHELIN;1 Star`, `2025`. |
| `contextualPrompts` | array | – | Occasion prompts, e.g. `ROMANTIC`, `BRUNCH`, `FUN`. |
| `wheelchairAccessible` | boolean | `false` | Only restaurants flagged wheelchair accessible. |
| `searchRadiusMiles` | integer | – | Radius around the location. **Widens as well as narrows** — a New York search is 637 at the default, 985 at 2 miles, 63 at half a mile. Ignored when `q` is set, because a term search runs nationwide. |
| `sort` | string | – | `best` / `rating` / `distance` / `newestArrivals`. |
| `reviewsPerRestaurant` | integer | `20` | How many reviews to fetch for each restaurant (paged automatically). Only used when reviews are enabled. |
| `reviewsSort` | enum | `newestReview` | Which reviews come first — one of `newestReview`, `highestRating`, `lowestRating`, `recommended`. |
| `detailConcurrency` | integer | `8` | How many restaurants to enrich in parallel (1-20). Raise for speed, lower to be gentler on the API and your proxy. |
| `proxyConfiguration` | object | Apify Proxy, RESIDENTIAL, US | Route requests through Apify Proxy. Search works from any egress, but <b>restaurant details are geo-gated to US IPs</b>, so the default is RESIDENTIAL with country US. |

#### Which cities and countries can I search?

Any place OpenTable's own autocomplete recognises — US metros and international markets
alike. Verified live against this Actor, 12-30 restaurants each:

| Location input | Returns restaurants in | `country` |
|---|---|---|
| `New York, NY` | Manhattan and the outer boroughs | United States |
| `Chicago, IL` | Chicago | United States |
| `Los Angeles, CA` | Los Angeles | United States |
| `Miami, FL` | Miami | United States |
| `London, United Kingdom` | London | United Kingdom |
| `Paris, France` | Paris | France |
| `Berlin, Germany` | Berlin | Germany / Deutschland |
| `Tokyo, Japan` | Chuo-ku, Minato-ku, Shibuya-ku | Japan / 日本 |
| `Sydney, Australia` | Sydney, Surry Hills, Barangaroo, Drummoyne | Australia |
| `Toronto, Canada` | Toronto, North York, Don Mills | Canada |
| `Mexico City, Mexico` | Ciudad de México | Mexico / México |
| `Dubai, United Arab Emirates` | Dubai, Business Bay | United Arab Emirates |

Both `"Boise, Idaho"` and `"Boise, ID"` forms resolve, as do plain city names and street
addresses. Note that `country` (and sometimes `city`) can come back in the local language —
Tokyo rows carry either `Japan` or `日本` — so normalise it if you group by country.

#### Example input

```jsonc
{
  "location": "New York, NY",
  "maxItems": 100,
  "includeDetails": false,
  "includeReviews": false,
  "reviewsPerRestaurant": 20,
  "reviewsSort": "newestReview",
  "detailConcurrency": 8,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}
```

***

### What does one row look like?

A row from a run with **both** deep-scrape options on (the detail and review fields are
absent when they are off):

```jsonc
{
  "id": "108541",
  "name": "Sofi Restaurant",
  "url": "https://www.opentable.com/restaurant/profile/108541",
  "cuisines": [
    "Italian"
  ],
  "priceBand": "$31 to $50",
  "priceBandId": "3",
  "rating": 4.7,
  "reviewCount": 159,   // the count OpenTable publishes, not its `count` sibling
  "neighborhood": "South Loop",
  "neighborhoodId": "82",
  "metroId": "3",
  "address": "616 South Dearborn",
  "city": "Chicago",
  "state": "IL",
  "postalCode": "60605",
  "country": "United States",
  "latitude": 41.8737957,
  "longitude": -87.629455,
  "images": [
    "https://resizer.otstatic.com/v4/photos/67602167-3?width=105&height=105",
    "https://resizer.otstatic.com/v4/photos/67602167-3?width=1280&height=1280",
    "…+6 more sizes of the same photo"
  ],
  "photosCount": 0,
  "description": "Born in Milan to a renowned restaurant family, Chef/Owner Maurizio Michi has always had a passion for cooking.\nThis Northern Italian Restaurant features clas…",
  "features": [
    "RESERVATION",
    "ONLINE_WAITLIST",
    "…+2 more"
  ],
  "firstNextAvailableTime": "2026-08-22T16:30",
  "phone": "3127535502",
  "phoneFormatted": "(312) 753-5502",
  "website": "http://www.sofichicago.com/",
  "menuUrl": null,
  "hours": "Mon–Thu, Sun 4:00 pm–10:00 pm\nFri, Sat 4:00 pm–10:30 pm",
  "diningStyle": "Casual Elegant",
  "dressCode": "Business Casual",
  "parkingInfo": "Street Parking",
  "paymentOptions": [
    "AMEX",
    "Discover",
    "…+2 more"
  ],
  "popularDishes": [
    {
      "name": "Gnocchi Genovese",
      "description": "Potato dumplings, basil pesto, green beans",
      "photoUrl": "https://resizer.otstatic.com/v4/photos/…",
      "reviewMentions": 11,
      "photoMentions": 3
    },
    "…+7 more"
  ],
  "popularDishNames": ["Gnocchi Genovese", "Tagliatelle Bolognese", "…+6 more"],
  "maxPartySize": 8,
  "maxAdvanceDays": 90,
  "isTakeoutEnabled": true,
  "hasGenderNeutralRestroom": true,
  "currencyCode": "USD",
  "resosInLastDay": 24,
  "reviewsFetched": 3,
  "reviews": [
    {
      "…": "16 fields"
    },
    {
      "…": "16 fields"
    },
    "…+1 more"
  ]
}
```

***

#### How much of the city did this run cover?

A search run reports its own coverage, so a short dataset is never ambiguous. The run's status message says it outright — *"Returned 50 of 440 matching restaurants (11%). Raise maxItems to collect the rest."* — and a `SEARCH_COVERAGE` record lands in the key-value store:

```json
{ "returned": 50, "totalMatching": 440, "totalAvailable": 440,
  "complete": false, "truncatedBy": "maxItems" }
```

`truncatedBy` distinguishes a `maxItems` cut from a spend-cap cut, so you can tell "that is every Italian restaurant in Brooklyn" from "that is the first 50 of 440".

### Data table

| Field | Type | From | Description |
| --- | --- | --- | --- |
| `id` | text | search | OpenTable restaurant ID |
| `name` | text | search | Name |
| `url` | link | search | Profile URL |
| `cuisines` | array | search | Cuisine labels |
| `priceBand` | text | search | Price band label, localised (e.g. `$31 to $50`, `£26 to £40`) |
| `priceBandId` | text | search | Price band number — the market-independent one |
| `rating` | number | search | Overall star rating, 1-5; `null` with no reviews |
| `reviewCount` | number | search | Diner reviews OpenTable publishes — the figure its own restaurant page shows |
| `neighborhood` | text | search | Neighbourhood |
| `neighborhoodId` | text | search | OpenTable neighbourhood ID |
| `metroId` | text | search | OpenTable metro (market) ID |
| `address` | text | search | Street address |
| `city` | text | search | City |
| `state` | text | search | State / region |
| `postalCode` | text | search | Postal code |
| `country` | text | search | Country, sometimes in the local language |
| `latitude` | number | search | Latitude |
| `longitude` | number | search | Longitude |
| `images` | array | search | Profile photo URLs — **one** photo, as 8 URLs at 7 distinct sizes: 105, 160, 208, 320, 640, 752 and 1280px, with 1280 served twice |
| `photosCount` | number | profile | Size of the restaurant's photo gallery. Only the profile carries it, so it is `null` on a plain search row and the real count whenever the profile was fetched — naming the restaurant directly (`restaurantUrls` / `restaurantNames`) or passing `includeDetails`. It is never `0` for "not fetched": the search payload reports `0` for every restaurant, including one whose profile reports 664 photos |
| `description` | text | search | Blurb (longer version when details are on) |
| `features` | array | search | Booking features, e.g. `RESERVATION`, `ONLINE_WAITLIST`, `DIRECT_MESSAGING`, `DINER_AI`, `TAKEOUT_TIPS_ENABLED` |
| `firstNextAvailableTime` | text | search | Next bookable slot, local time |
| `phone` | text | details | Phone, digits only |
| `phoneFormatted` | text | details | Phone, formatted |
| `website` | link | details | Website |
| `menuUrl` | link | details | External menu link |
| `hours` | text | details | Opening hours |
| `diningStyle` | text | details | e.g. Casual Dining, Casual Elegant |
| `dressCode` | text | details | e.g. Casual Dress, Business Casual, Resort Casual |
| `parkingInfo` | text | details | e.g. Valet, Street Parking, None |
| `paymentOptions` | array | details | e.g. AMEX, Visa, Mastercard, Discover, Diners Club |
| `popularDishes` | array | details | One object per dish: `name`, `description`, `photoUrl`, `reviewMentions`, `photoMentions` |
| `popularDishNames` | array | details | The same dish names as a flat list, for simpler exports |
| `menuPhotosCount` | number | details | Diner-uploaded menu photos on the profile |
| `maxPartySize` | number | details | Largest party bookable online |
| `maxAdvanceDays` | number | details | How far ahead bookings open |
| `isTakeoutEnabled` | boolean | details | Takeout available |
| `hasGenderNeutralRestroom` | boolean | details | Gender-neutral restroom |
| `currencyCode` | text | details | e.g. `USD` |
| `resosInLastDay` | number | details | Reservations made in the last 24 hours |
| `menus` | array | menu | The full OpenTable-hosted menu: `menu → sections → items → variations`, with prices, currency and the data provider |
| `menuCount` | number | menu | How many menus this restaurant has. `null` unless the menu route ran, so `0` means OpenTable genuinely hosts none |
| `menuItemCount` | number | menu | Total items across all its menus. `null` unless the menu route ran, as with `menuCount` |
| `availabilitySlots` | object | availability | Bookable slots plus `minPartySize`, `maxPartySize`, `maxDaysInAdvance` and `noTimesReasons` |
| `reviewsFetched` | number | reviews | How many reviews this row carries |
| `reviews` | array | reviews | The reviews themselves — 16 fields each, below |

#### What is in each review?

`reviews[]` carries these 16 fields: `id`, `author`, `authorMetro` (the reviewer's home
market), `postedDate`, `reservationDate`, `text`, `overallRating`, `food`, `service`,
`ambience`, `value`, `noise`, `positiveFeedback`, `negativeFeedback`, `dinerIsVIP` and
`loyaltyTier`.

#### Which cuisines does OpenTable use?

`cuisines` carries OpenTable's own labels, and a restaurant can have several. An eight-city
sample (New York, Chicago, Los Angeles, Miami, London, Paris, Tokyo, Sydney and Toronto)
returned 68 distinct labels. The most common:

Italian · American · Japanese · French · Steakhouse · Mediterranean · Contemporary American · Korean · Steak · Chinese · Cocktail Bar · Seafood · Sushi · Mexican · Greek · Modern Australian · Spanish · Bar / Lounge / Bottle Service · Café · European · Tapas / Small Plates · Australian · Contemporary Italian · Thai · Argentinean · Bistro · Brazilian · British · Comfort Food · Fusion / Eclectic · Lebanese · Middle Eastern · Pizzeria · Pizza Bar · Pub · Gastro Pub · Southeast Asian · Vietnamese · Teppanyaki · Yakiniku · Izakaya · Sichuan · Persian · Latin American · Caribbean · Cuban · Ethiopian · German · Irish · Southern · Soul food · Vegetarian / Vegan

Set `q` to any of these, or to a dish (`ramen`) or a restaurant name (`Nobu`), to narrow a
search. Restaurants OpenTable has not classified come back as `Unspecified`. Note that a
brand-name `q` can pull in matches outside your `location` — searching `Nobu` around New
York also returns Nobu Miami and Nobu Las Vegas — so filter on `city` or `metroId` if you
need the results strictly local.

#### What do these OpenTable terms mean?

- **Price band (`priceBand` / `priceBandId`)** — OpenTable's per-person spend bracket. The
  **label is localised to the market**: band `3` is `$31 to $50` in the US, `£26 to £40` in
  London, `€31 to €50` in Paris, `¥5,001 to ¥10,000` in Tokyo and `CAN$31 to CAN$50` in
  Toronto. Compare across countries on `priceBandId`, never on the label.
- **Metro (`metroId`)** — OpenTable's market ID, the grouping the app calls a "metro". Every
  restaurant in one search area shares it, so it is a stable key for "this city": `3` is
  Chicago, `6` Los Angeles, `8` New York, `17` Miami, `72` London, `74` Toronto, `201`
  Tokyo, `255` Sydney, `3408` Paris.
- **`firstNextAvailableTime`** — the earliest table OpenTable can seat, as local time at the
  restaurant. It is a snapshot at the moment of the run, not a schedule.
- **`resosInLastDay`** — reservations booked through OpenTable in the previous 24 hours. The
  closest thing the API gives to a popularity signal.
- **`availabilitySlots`** — one object per restaurant: the slots found, `slotCount`,
  `requestedFor` (the date and time the answer is for), the party-size and booking-window
  limits, and `noTimesReasons` — OpenTable's own explanation when the list is empty
  (`AboveMaxPartySize`, `TooFarInAdvance`, `NoTimesExist`). **No slots, no reasons and
  zeroed limits is itself an answer**: the restaurant is not on OpenTable's booking
  network, so nothing can be booked through it at any time. Measured 2026-10-26 on
  Balthazar, whose OpenTable page says exactly that.
- **`reviews`** — a list of review objects on the restaurant's own row, not one row per
  review, and `reviewsFetched` counts them. That keeps one row per restaurant, but it
  makes a CSV export wide: measured 2026-10-26, 97 reviews came out as 1,782 columns, and
  a 9-menu restaurant as 2,000. For a flat export, pass `fields=` on the API or use the
  `Diner reviews` dataset view and read the JSON.
- **Noise (`noise` on a review)** — diners rate how loud the room was on the same 1-5 scale
  as food, service, ambience and value; a low number means quiet.
- **`features`** — which booking mechanics the restaurant has switched on, not amenities:
  `RESERVATION` (bookable), `ONLINE_WAITLIST`, `DIRECT_MESSAGING`, `DINER_AI`,
  `TAKEOUT_TIPS_ENABLED`.
- **Dining style vs dress code** — `diningStyle` is the format (e.g. Casual Dining, Casual
  Elegant); `dressCode` is what to wear (e.g. Casual Dress, Business Casual, Resort Casual).
- **`maxAdvanceDays`** — the booking window: how many days ahead the restaurant opens
  tables. 14, 30, 42 and 90 are all common.

***

### How much does it cost?

This actor uses **pay-per-event** pricing: you pay for the data it actually returns,
and nothing for a slice that came back empty.

**See the [Pricing tab](https://apify.com/rl1987/opentable-api-scraper/pricing) for the
current per-event prices.** They vary by your Apify plan, so the tab shows what *you*
would pay — which a table here could not.

What gets billed:

- **the restaurant record** — one charge per restaurant returned by a search
- **the profile** — one charge per restaurant whose own profile page was read
- **the menus** — one charge per restaurant that actually has an OpenTable-hosted menu
- **the reviews** — charged per review returned, so you pay for the number you asked for
- **the availability check** — one charge per completed check for a restaurant, date and
  party size. "No tables" is a completed check and is billed: the question was answered.
  A restaurant that is not on OpenTable's booking network answers with no times and no
  reason, which is also an answer — see `availabilitySlots` below. A check that could not
  be completed at all is not billed

A plain search bills only the first of those. Each deep-scrape option is off by default
and costs one extra request per restaurant, so you opt in to what you need.

***

### Use the OpenTable scraper via API

Run it programmatically with the [Apify API](https://docs.apify.com/api/v2):

```bash
curl -X POST "https://api.apify.com/v2/acts/rl1987~opentable-api-scraper/runs?token=YOUR_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
  "location": "New York, NY",
  "maxItems": 100,
  "includeDetails": false,
  "includeReviews": false,
  "reviewsPerRestaurant": 20,
  "reviewsSort": "newestReview",
  "detailConcurrency": 8,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}'
```

Or with the Apify CLI: `apify call rl1987/opentable-api-scraper --input-file=input.json -o`.

***

### FAQ

**Do I need an OpenTable account or API key?** No. The Actor talks to OpenTable's own mobile API as an anonymous client — there is nothing to register. An Apify account is all you need.

**Can I get restaurant phone numbers?** Yes, with **Include restaurant details** on: `phone` (digits) and `phoneFormatted`. That route is geo-gated, which is why the proxy defaults to Apify Residential with country US — leave it as it is and phones work out of the box.

**Does it cover restaurants outside the US?** Yes. London, Paris, Berlin, Tokyo, Sydney, Toronto, Mexico City and Dubai were all verified live. Price-band labels come back in the local currency, so compare markets on `priceBandId`.

**How many restaurants can one run return, and how do I cap the cost?** The API pages 30 at a time and the Actor follows pagination until OpenTable's own `total` is reached. `maxItems` caps it (default 100, `0` = everything), and because billing is per row, that cap is also your budget cap.

**Can I schedule it to track ratings or availability over time?** Yes — use Apify **Schedules**. `rating`, `reviewCount`, `resosInLastDay` and `firstNextAvailableTime` are all snapshots at run time, so a daily run gives you the series.

**What happens if my search returns nothing, or OpenTable blocks the run?** Neither is treated as a crash. An empty search, an unusable input and a search OpenTable itself refuses each finish with a status message explaining what to change. If OpenTable blocks every exit IP after retries, the run says so plainly in its status rather than handing you a silent empty dataset, and if a later page fails the Actor keeps the records it already collected.

**Are the images the full photo gallery?** No. `images` is the restaurant's profile photo at several render sizes, not several different photos: 8 URLs covering 7 distinct widths from 105px to 1280px, with 1280 served twice. `photosCount` tells you how big the real gallery is, and only the profile carries it — measured 2026-10-26: 492 for The Dining Room at Gramercy Tavern and 400 for Balthazar, both matching their OpenTable pages. On a plain search row it is `null`, because the search payload reports `0` for every restaurant whatever its real gallery (measured 2026-11-08: `0` for Hinata Sushi, whose profile reports 664). Pass `includeDetails`, or name the restaurant directly, to get the real number.

**Is scraping OpenTable legal?** You are responsible for how you use the data. Collect only public restaurant data and comply with OpenTable's Terms and applicable law (e.g. GDPR/CCPA where reviews and reviewer names count as personal data).

***

### How it works

The Actor impersonates OpenTable's official app and calls its mobile API the same way the app does: a token request, the app's autocomplete to turn your `location` into the coordinates OpenTable itself would search, then the paged personalizer search — plus the profile and reviews endpoints when you ask for them. Each response is normalised into the flat record above. No browser, no HTML scraping.

***

### Good to know

- Collect only public restaurant data and respect OpenTable's Terms of Service and robots policy.
- Reviews carry reviewer names and home metros. For personal data, comply with GDPR/CCPA and applicable law — you are the data controller.
- Fields can be `null` when OpenTable doesn't expose them for a given restaurant — `rating` is `null` before the first review, and `menuUrl` is often empty.

***

### Found this useful?

Give the **OpenTable Restaurant Scraper** a star on Apify and check out my other `apify-*-api-scraper` actors.

# Actor input Schema

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

Start with <b>Search</b> to find restaurants by area. The other four modes each fetch <i>one</i> kind of data for a list of restaurants you supply below — so a scheduled run tracking menu changes doesn't pay for reviews it didn't ask for. In Search mode the <b>Include…</b> toggles add the same data per restaurant.

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

City, area, or address to search around (e.g. "New York, NY", "San Francisco"). Resolved to coordinates at run time.

## `lat` (type: `number`):

Optional latitude override; skips the location lookup.

## `lon` (type: `number`):

Optional longitude override; skips the location lookup.

## `q` (type: `string`):

Optional cuisine or keyword (e.g. "sushi", "Indian").

## `includeDetails` (type: `boolean`):

Fetch each restaurant's full profile: <b>phone number</b>, website, opening hours, dining style, dress code, parking, payment options, menu link and popular dishes. One extra request per restaurant, billed as a separate detail-row event. <b>Requires a US proxy</b> — this route is geo-gated and simply hangs from non-US IPs.

## `includeMenu` (type: `boolean`):

Fetch the complete OpenTable-hosted menu for each restaurant - every menu, section, item, description, price and size variation. Adds one API call per restaurant. <b>Separate from <code>menuUrl</code></b>, which is the menu on the restaurant's own website: that field comes from the profile, so it needs <b>includeDetails</b>. A restaurant can have either source, both or neither, so enable both options to capture both.

## `includeReviews` (type: `boolean`):

Fetch diner reviews per restaurant — text, author, dates and the per-category ratings (food, service, ambience, value, noise). Billed per review returned, so <code>reviewsPerRestaurant</code> is also your cost dial for this slice.

## `includeAvailability` (type: `boolean`):

Fetch bookable reservation slots per restaurant for the <b>date</b>, <b>time</b> and <b>party size</b> below: slot times, seating type, dining area, loyalty points, and whether a credit card is required (hold vs deposit). Adds one API call per restaurant. <b>Needs a US proxy</b> — like the property profile route, this one hangs from non-US IPs rather than returning an error. A completed check is billed even when nothing is bookable — including a restaurant that is not on OpenTable's booking network, which answers with no times and no reason. That is an answer; a check that could not be completed is not billed.

## `sort` (type: `string`):

Order of the search results. The full set from the app's own SortType enum; all four verified to change the ordering.

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

Filter by cuisine, applied by OpenTable rather than after the fact, so you do not pay for rows you did not want. Use display names (<code>Italian</code>, <code>Steakhouse</code>, <code>Seafood</code>) and they are resolved to OpenTable's ids automatically; raw cuisine UUIDs also work. A name not offered for your search area is reported and ignored.

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

Filter by neighbourhood, e.g. <code>Chelsea</code>, <code>Astoria</code>. Names are resolved to OpenTable's ids (<code>n_hood:195</code>, <code>c_hood:292</code>), which you may also pass directly.

## `priceBands` (type: `array`):

OpenTable price bands. Verified values and their labels: <code>2</code> = $30 and under, <code>3</code> = $31 to $50, <code>4</code> = $50 and over. (<code>1</code> returns nothing - the app's own PriceBand enum only has 2, 3 and 4.) On a 637-match New York search these left 260, 244 and 133 respectively.

## `awardTags` (type: `array`):

Filter to award holders, e.g. <code>MICHELIN</code>, <code>MICHELIN;1 Star</code>, or a year like <code>2025</code>. The exact values available are listed in each search response's own facets.

## `contextualPrompts` (type: `array`):

OpenTable's own occasion prompts, e.g. <code>ROMANTIC</code>, <code>BRUNCH</code>, <code>FUN</code>, <code>PIZZA</code>.

## `wheelchairAccessible` (type: `boolean`):

Only restaurants OpenTable flags as wheelchair accessible.

## `searchRadiusMiles` (type: `integer`):

Radius around the search location, in miles. OpenTable's own app offers 2, 10, 25 and 50. <b>This widens as well as narrows:</b> in a dense city the default radius is tighter than 2 miles, so a New York search went from 637 matches to 985 at a 2-mile radius and down to 63 at half a mile. Leave empty for OpenTable's default. <b>Ignored when a search term (<code>q</code>) is set</b> - a term search runs nationwide, which is OpenTable's behaviour.

## `restaurantUrls` (type: `array`):

Scrape these restaurants directly instead of searching. Accepts a bare numeric ID (<code>42265</code>), a profile URL (<code>https://www.opentable.com/restaurant/profile/42265</code>) and any ordinary opentable.com restaurant URL (<code>https://www.opentable.com/del-friscos-double-eagle-steak-house-new-york-city</code>) - slug URLs are resolved automatically. Matching is exact, so a stale or truncated slug is skipped with a message rather than guessed at. When set, <b>location</b>/<b>q</b> are ignored.

## `restaurantNames` (type: `array`):

Resolve restaurants by name instead of URL, one per line as <code>Name, City</code> — e.g. <code>Brooklyn Chop House Times Square, New York</code>. Resolution is a name match within that city: <b>an ambiguous name is reported with the near-misses rather than guessed</b>, so two restaurants of the same name in one city will not resolve — pass the ID or profile URL for those. Costs two requests per entry.

## `reviewsPerRestaurant` (type: `integer`):

How many reviews to fetch for each restaurant (paged automatically). Only used when reviews are enabled.

## `reviewsSort` (type: `string`):

Which reviews come first.

## `date` (type: `string`):

Date to check availability for, <code>YYYY-MM-DD</code>. Defaults to tomorrow. Each restaurant has its own booking horizon (<code>maxDaysInAdvance</code>; 90 and 365 days on two live samples) — beyond it you get no slots and <code>noTimesReasons: \["TooFarInAdvance"]</code> rather than a silent empty list.

## `daysAhead` (type: `integer`):

Check availability this many days from the day the run starts - <code>0</code> today, <code>1</code> tomorrow, <code>7</code> a week out. Use this instead of <b>date</b> for scheduled monitoring: a relative target stays correct every run, where a fixed date goes stale. Setting both <b>date</b> and <b>daysAhead</b> is rejected. Past a restaurant's own booking horizon you get <code>noTimesReasons: \["TooFarInAdvance"]</code> rather than a silent empty list.

## `time` (type: `string`):

Time of day, <code>HH:MM</code> (24-hour). Defaults to <code>19:00</code>.

## `partySize` (type: `integer`):

Number of diners. Defaults to 2. Above a restaurant's <code>maxPartySize</code> you get no slots and <code>noTimesReasons: \["AboveMaxPartySize"]</code>.

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

Maximum number of restaurants to return (0 = all pages).

## `detailConcurrency` (type: `integer`):

How many restaurants to enrich in parallel (1-20). Raise for speed, lower to be gentler on the API and your proxy.

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

Route requests through Apify Proxy. Defaults to <b>RESIDENTIAL, country US</b>, which is what the restaurant-details option needs: that route is geo-gated and hangs (rather than erroring) from non-US IPs — measured 5/5 phone numbers in 24s on residential US vs 3/5 in 133s on datacenter. Search and reviews are not gated, so for a search-only run you can switch to datacenter to go faster and spare your residential GB quota.

## Actor input object example

```json
{
  "mode": "search",
  "location": "New York, NY",
  "q": "",
  "includeDetails": false,
  "includeMenu": false,
  "includeReviews": false,
  "includeAvailability": false,
  "sort": "",
  "cuisines": [],
  "neighborhoods": [],
  "priceBands": [],
  "awardTags": [],
  "contextualPrompts": [],
  "wheelchairAccessible": false,
  "restaurantUrls": [],
  "restaurantNames": [],
  "reviewsPerRestaurant": 20,
  "reviewsSort": "newestReview",
  "time": "19:00",
  "partySize": 2,
  "maxItems": 100,
  "detailConcurrency": 8,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}
```

# Actor output Schema

## `results` (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",
    "location": "New York, NY",
    "q": "",
    "cuisines": [],
    "neighborhoods": [],
    "priceBands": [],
    "awardTags": [],
    "contextualPrompts": [],
    "restaurantUrls": [],
    "restaurantNames": [],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "US"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("rl1987/opentable-api-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",
    "location": "New York, NY",
    "q": "",
    "cuisines": [],
    "neighborhoods": [],
    "priceBands": [],
    "awardTags": [],
    "contextualPrompts": [],
    "restaurantUrls": [],
    "restaurantNames": [],
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "US",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("rl1987/opentable-api-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",
  "location": "New York, NY",
  "q": "",
  "cuisines": [],
  "neighborhoods": [],
  "priceBands": [],
  "awardTags": [],
  "contextualPrompts": [],
  "restaurantUrls": [],
  "restaurantNames": [],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}' |
apify call rl1987/opentable-api-scraper --silent --output-dataset

```

## MCP server setup

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