# OpenTable Scraper – Restaurants, Ratings & Demand (`simple.actors/opentable-restaurants`) Actor

Scrape OpenTable restaurants worldwide: address, phone, coordinates, neighbourhood, cuisine, price band, rating, review count, photos and OpenTable's own recent-reservation count — a direct measure of how busy a restaurant is. Slot reading is optional. Unofficial.

- **URL**: https://apify.com/simple.actors/opentable-restaurants.md
- **Developed by:** [Simple Actors](https://apify.com/simple.actors) (community)
- **Categories:** Travel, Business, Lead generation
- **Stats:** 3 total users, 3 monthly users, 89.3% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.30 / 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.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## OpenTable Scraper – Restaurants, Ratings & Demand

Scrape OpenTable restaurant data anywhere in the world. Search by term and city
and get the full record for each restaurant — coordinates, address, phone,
neighbourhood, cuisine, price band, rating, review count, photos — plus
**OpenTable's own recent-reservation count**, a direct measure of how busy a
restaurant is. Built for restaurant market research, competitor and demand
tracking, hospitality lead lists and location scouting.

Advanced: if you supply your own browser session and a residential proxy, it can
also read bookable reservation times. See *Reading bookable slots* below for what
that involves before you count on it.

Unofficial. Not affiliated with OpenTable.

### Features

- **Global restaurant search** — 441 OpenTable markets and 17,280 towns and
  neighbourhoods resolve by name; London, Paris, Lisbon, Sydney, Toronto and
  Mexico City all work.
- **The demand signal** — `recentReservationCount`, OpenTable's own "Booked N
  times today" figure, on every search result. No reservation lookup needed.
- **Full restaurant record** — id, name, URL, latitude/longitude, address, city,
  state, postcode, neighbourhood, cuisine, dining style, price band, rating,
  review count, phone, photos and description.
- **Bookable versus merely listed** — `isBookable` and `restaurantType` tell a
  real booking flow apart from a directory entry that books elsewhere.
- **Optional profile-page details** — website, ~17 amenities, opening hours,
  executive chef, dress code, parking, payment options, all cuisines, country,
  reviews, popular dishes and special experiences.
- **Track a fixed list by ID** — pass `restaurantIds` and skip the search
  entirely, one run for the whole list.
- **Search by exact coordinates** — `latitude` and `longitude` override the city.
- **Optional bookable slots** — every open time with a booking token, plus a
  prime-window fill score normalised by each restaurant's booking granularity.
- **Ambiguous places are refused, not guessed** — OpenTable's bare "brooklyn" is
  Brooklyn, *Ohio*, so a name that could mean several places returns a row
  listing the choices instead of fifty restaurants from the wrong state.
- **Nulls mean "not published"**, never zero and never an empty string.

### Input

```json
{
  "term": "steak",            // what to search for — required unless using restaurantIds
  "city": "Miami",            // write it as you would type it into OpenTable's location box
  "state": "FL",              // qualifies an ambiguous name: "Brooklyn, NY", "Lisbon, Portugal"
  "maxItems": 50,             // restaurants to return — leave at 50, the extras are nearly free
  "includeDetails": false,    // also read each restaurant's own profile page (one page load each)
  "includeAvailability": false // read bookable slots — needs session cookies, see below
}
```

Track a known list instead, with no search at all:

```json
{ "restaurantIds": ["34009", "1470478", "7868"] }
```

| Field | Type | Default | What it does |
|---|---|---|---|
| `term` | string | `steak` | **Required** unless `restaurantIds` is set. What to search for. |
| `city` | string | — | Where to search. Matched against OpenTable's own directories. |
| `state` | string | — | State or country, to disambiguate a city name. |
| `latitude` / `longitude` | number | — | An exact point. Both or neither; overrides `city`. |
| `restaurantIds` | array | `[]` | OpenTable IDs to read directly, skipping the search. |
| `maxItems` | integer | `50` | Maximum restaurants to return. |
| `includeDetails` | boolean | `false` | Read each restaurant's profile page for website, amenities, hours and more. |
| `includeAvailability` | boolean | `false` | Read bookable slots. Needs `sessionCookies` and a residential `availabilityProxy`. |
| `date` / `time` / `partySize` | string/int | — / `19:00` / `2` | The reservation being checked. |
| `primeWindowStart` / `primeWindowEnd` | string | `18:00` / `21:00` | The window scored by `primeWindowOpenPercent`. |
| `sessionCookies` | string | — | Your own browser session, for slot reading only. |
| `userAgent`, `secChUa`, `secChUaPlatform`, `csrfToken` | string | — | Must describe the same browser the cookies came from. |
| `databaseRegion` | string | `NA` | `NA`, `EMEA` or `APAC`. Affects availability only. |
| `includeRaw` | boolean | `false` | Attach OpenTable's untouched payloads. |
| `proxy` | object | Unblocker | For the search page. |
| `detailProxy` | object | Apify proxy | For profile pages. |
| `availabilityProxy` | object | Residential | For the reservation lookup — datacenter IPs are refused outright. |

`city` is where the search happens — write it the way you would type it into
OpenTable's own location box. It is matched against OpenTable's own directories,
so the search lands where OpenTable puts that place rather than where some other
map does, and the run log names what it matched along with anything else that
was close. **No session, cookies or key are needed for any of this.**

Two levels resolve, in this order:

- **Markets** (441 of them) — "Miami" means the whole Miami/Ft Lauderdale market,
  which is what you want when you are searching a city.
- **Towns and neighbourhoods** (17,280) — "Brooklyn", "Napa" and "SoHo" are not
  markets, so these are searched around their own point instead.

Coverage is global: London, Paris, Lisbon, Sydney, Toronto and Mexico City all
resolve as markets.

Below market level, **a name that could mean several places is refused rather
than guessed** — the search is not run, and you get one row naming the choices
so you can pick one. Add the state or country the way OpenTable writes it:
`Brooklyn, NY`, `Cambridge, MA`, `Lisbon, Portugal`. This is deliberate:
OpenTable's unqualified names are not the famous ones (its bare "brooklyn" is
Brooklyn, **Ohio**), so guessing would hand you fifty real restaurants from the
wrong state with nothing in the rows to say so. If OpenTable lists nowhere by
that name at all, you get a row saying that instead.

Want an exact point instead? Set `latitude` **and** `longitude` — both, or
neither — and they override `city`. Set none of the three and OpenTable places
the search by the address your proxy happens to use, which is not something a
run controls; the log warns when that happens.

Already know the restaurants you want? Put their IDs in `restaurantIds` and leave
`term` empty — see *Tracking a known set of restaurants* above.

### Output

Every run returns one dataset item per restaurant:

| Field | Example |
|---|---|
| `restaurantId` | `1017331` |
| `name` | `Brooklyn Chop House - Downtown` |
| `url` | `https://www.opentable.com/r/brooklyn-chop-house-new-york` |
| `latitude` / `longitude` | `40.7114624` / `-74.0059409` |
| `address`, `city`, `state`, `postCode` | `150 Nassau St`, `New York`, `NY`, `10038` |
| `neighborhood` | `TriBeCa` |
| `cuisine`, `diningStyle` | `Steakhouse`, `Fine Dining` |
| `priceBand`, `currencySymbol` | `$31 to $50`, `$` |
| `rating`, `reviewCount` | `4.1`, `3045` |
| **`recentReservationCount`** | `33` |
| `phone` | `(212) 619-1200` |
| **`isBookable`** | `true` — see *Bookable, or just listed?* below |
| `restaurantType` | `GuestCenter` (bookable) or `Listing` (directory entry) |
| `permanentlyClosed` | `true` only where OpenTable says so |
| `description`, `topReview` | plain text, tags and entities resolved |
| `photos` | array of image URLs |
| `hasTakeout`, `hasPrivateDining`, `hasBar`, `hasOutdoorSeating`, `maxPartySize` | |

A field that OpenTable did not publish comes back **`null`**, never `0` and never
an empty string. That distinction carries weight here: a restaurant with
`recentReservationCount: null` is one OpenTable does not publish a figure for,
which is not the same as a restaurant nobody booked.

#### The demand signal

`recentReservationCount` is OpenTable's own published figure for how many times a
restaurant has been booked recently. It is the cheapest honest measure of how busy
a place is: it needs no reservation lookup, it is a number the restaurant's own
listing states rather than something inferred, and it comes back on every search.
In one New York sample it ranged from 6 to 134 across five restaurants in the same
city — it discriminates.

It is OpenTable's "Booked N times today" figure, exactly — verified against the
rendered page. Two things to know before you build baselines on it:

- **It does not reset at midnight, despite the "today".** We polled restaurants
  across their own local midnight and none of them reset: readings either side of
  the boundary were 41/41, 12/12, 168/169, 100/101. It behaves as a rolling
  trailing count that drifts by a few in either direction. So compare a restaurant
  against itself at the same hour on the same weekday, and do not expect a daily
  zero to anchor to.
- **It is only ever published for bookable restaurants.** A directory listing has
  no figure at all — `null` — because there is nothing on OpenTable to book. Read
  `isBookable` before reading a low number as a quiet restaurant.

The rest of the record is stable run to run: three identical searches minutes
apart returned the same 50 restaurants, in the same order, with the same figures.

If you want slot-level detail on top of that, read on.

#### Bookable, or just listed?

Not every restaurant on OpenTable can be booked on OpenTable. Some are directory
entries: the restaurant is real and often thriving, but it takes its reservations
somewhere else, and OpenTable lists it without a booking flow. Balthazar in New
York (`7868`) is one — it books on Resy.

Those records look like very quiet restaurants if you do not know to look: no
demand figure, no maximum party size, usually no phone. **`isBookable`** tells them
apart, and `restaurantType` carries OpenTable's own word for the record beside it.

`isBookable` is `null` — never a confident `false` — if OpenTable ever uses a record
type this Actor has not measured. If you need to act on that case, `restaurantType`
has the raw value.

### Full details

Turn on **Read full details** and each restaurant's own profile page is read as
well, adding everything the search page leaves out:

| Field | Example |
|---|---|
| **`website`** | `http://www.bond45ny.com/` — the restaurant's own site |
| **`amenities`** | `["Bar/Lounge","Beer","Cocktails","Full Bar","Gluten-free Options","Happy Hour","Patio/Outdoor Dining","Vegan","Wheelchair Access", …]` |
| `hoursOfOperation` | `Mon, Sun 7:00 am-10:00 pm` / `Tue-Thu 7:00 am-11:30 pm` |
| `executiveChef` | `Brando De Oliveira` |
| `dressCode` | `Smart Casual` |
| `parkingInfo` | `None` |
| `paymentOptions` | `["AMEX","Diners Club","Discover","JCB","Mastercard","Visa"]` |
| `cuisines` | `["Italian","American"]` — all of them, not just the primary |
| `country` | `United States` — the search record has no country field at all |
| `totalReviewCount` / `textReviewCount` | `745` / `10761` |
| `reviews` | recent review text |
| `popularDishes` | dish name, description and how many reviews mention it |
| `experiences` | prix fixe menus, restaurant weeks and other special offers |
| `publicTransit`, `crossStreet`, `cateringDetails` | as the restaurant wrote them |
| `orderOnlineLink`, `facebookUrl`, `maxAdvanceDays` | |

`amenities` is OpenTable's full "Additional" list — around seventeen entries on a
well-filled profile. The `hasBar` / `hasOutdoorSeating` / `hasTakeout` /
`hasPrivateDining` booleans stay beside it because those come off a search result
too, without paying for the page.

**This costs one page load per restaurant**, where a search costs one page load
for fifty. That is why it is a separate charge and why it is much slower — a
detailed run of 50 restaurants reads 51 pages. Leave it off unless you need
these fields.

### Tracking a known set of restaurants

Put OpenTable restaurant IDs in **`restaurantIds`** and each one is read from its
own OpenTable page — the whole record, in a single run. No search, no `term`, no
location:

```json
{ "restaurantIds": ["34009", "1470478", "7868"] }
```

This is the mode to schedule if you are watching a fixed list of venues and want a
fresh `recentReservationCount` for each. One run covers the whole list rather than
one run per restaurant.

It is charged at the ordinary **Restaurant** rate — $0.0003 each — with the flat
start charge once per run, not once per restaurant. Ten venues in one run is
$0.0105; the same ten as ten separate searches is $0.225.

Add **Read full details** if you also want `website`, `amenities`,
`hoursOfOperation` and the rest; that is charged separately. A sensible split is
to run with details once when you first resolve a venue, and without them on the
schedule that only needs a fresh `recentReservationCount`.

Two fields cannot come this way, because the page does not carry them:
`topReview` and `restaurantAvailabilityToken`. Everything else matches what a
search would have returned for the same restaurant.

### Reading bookable slots

Turn on **Read bookable slots** and set a date, time and party size. Each item then
also carries:

| Field | Meaning |
|---|---|
| `available` | `true` / `false`, or `null` if the restaurant was not in the answer |
| `slots` | every open time, each with `time`, `offsetMinutes` and a booking token |
| `slotCount` | how many times are open in the three hours either side |
| `bookingGranularityMinutes` | whether this restaurant books on the quarter or half hour |
| `primeWindowOpenSlots` / `primeWindowCapacity` / `primeWindowOpenPercent` | how full the prime window is |
| `noTimesReasons` | why there are no times, when there are none |

`primeWindowOpenPercent` is normalised by each restaurant's own booking
granularity, so a place that books on the half hour is not scored as half empty
against one that books on the quarter hour. The window defaults to 18:00–21:00 and
is configurable — move it to score a lunch service instead.

#### This needs a session you supply

OpenTable answers its reservation endpoint only for a browser session that is
already valid. This Actor does not manufacture one, so you provide it:

1. Open [opentable.com](https://www.opentable.com) in your browser and search any city.
2. Open DevTools → **Network**, and click any request to `/dapi/fe/gql`.
3. Copy the whole **`cookie`** request header.
4. Paste it into the **Session cookies** input.

A session stays usable for roughly 40 minutes. That is far less limiting than it
sounds, because restaurants are batched **10 per request** — one session
comfortably covers thousands of restaurants in a single run.

If availability starts coming back refused, the session has expired: copy a fresh
one. If it keeps failing with a fresh session, also copy the `sec-ch-ua` header and
`user-agent` from the same request into the matching inputs — they have to describe
the same browser the cookies came from.

Leave **Session cookies** empty and you still get every restaurant field above,
including `recentReservationCount`, with nothing to set up.

Turn **Read bookable slots** on *without* a session and the run does not fail. You
get every restaurant record as usual, and each row carries `availabilityError`
(`no_session`) plus `availabilityErrorDescription` saying what was missing. The
slot fields — `available`, `slots`, `slotCount`, `primeWindowOpenPercent` — are
left off those rows entirely rather than returned empty, so a restaurant we could
not ask about is never confusable with one that is fully booked. **If you act on
availability, key off the presence of `available`, not off its value.**

#### It also needs the right kind of IP

Slot reading has a second requirement that has nothing to do with your session.
OpenTable refuses datacenter IP addresses on this endpoint outright — the
connection is dropped before your cookies are even looked at — so the reservation
lookup needs residential egress. Set **Availability proxy** to a residential group.

This is separate from the main proxy on purpose: the search page and the
reservation lookup cannot share one setting. The Unblocker reads the search page
happily but carries GET requests only, and answers the reservation POST with a
timeout and then a 502.

If you only want restaurant records and the demand count, none of this applies —
discovery works on the default settings.

### How to use

#### From Apify Console

1. Open the Actor and click **Try for free** / **Start**.
2. Put what you are looking for into **Search term** (`steak`, `sushi`,
   `italian`) and where into **City** — write it the way you would type it into
   OpenTable's own location box.
3. If the city name could mean several places, add **State or country** —
   `Brooklyn, NY`, `Lisbon, Portugal`. Ambiguous names are refused rather than
   guessed, and the row tells you the choices.
4. Leave **Maximum restaurants** at 50; you pay for the page load either way, so
   the extra records are nearly free.
5. Turn on **Read full details** only if you need website, amenities and opening
   hours — it costs one page load per restaurant.
6. Click **Start**, then open the **Dataset** tab and export as JSON, CSV or
   Excel.

**Check the first row for an `error` key** before treating the dataset as
results — a bad input finishes as a successful run with one explanatory row.

#### From the API

```bash
curl -s "https://api.apify.com/v2/acts/simple.actors~opentable-restaurants/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"term": "steak", "city": "Miami", "maxItems": 50}'
```

Tracking a fixed venue list on a schedule, with the JavaScript client:

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('simple.actors/opentable-restaurants').call({
    restaurantIds: ['34009', '1470478', '7868'],   // no search, no term, no city
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const r of items) {
    if (r.error) continue;                          // input problem, not a restaurant
    console.log(r.name, r.recentReservationCount, r.isBookable);
}
```

### Use cases

- **Restaurant demand and busyness tracking** — schedule a run over a fixed
  `restaurantIds` list and trend `recentReservationCount` per venue, comparing
  each restaurant against itself at the same hour on the same weekday.
- **Hospitality market research** — pull every restaurant in a market with
  rating, review count, cuisine and price band to size and segment a local
  dining scene.
- **Lead lists for restaurant suppliers and tech** — turn a city search into a
  list of venues with phone numbers, and with `includeDetails` their own
  websites, for outreach.
- **Location scouting and competitor mapping** — coordinates, neighbourhood and
  price band for every restaurant near a point, using `latitude`/`longitude`.
- **Menu, amenity and dining-experience research** — `includeDetails` returns
  amenities, popular dishes, dress code, opening hours and special experiences.
- **Reservation availability monitoring** — with your own session and a
  residential proxy, track how full the prime window is across a set of venues.

### Usage notes

#### What it costs

**$0.45 per 1,000 restaurants.**

| Event | Price |
|---|---|
| Actor start | $0.0075 per run |
| Restaurant | $0.0003 each |
| Full details | $0.004 each (optional) |

A full run returns 50 restaurants for **$0.0225** — that is where the $0.45 per
1,000 comes from.

**A run that fails costs you nothing.** The start charge is only levied once the
search page has actually been read, so a run that could not reach OpenTable, or
that you cancelled, is not billed. Restaurants are charged as they are written, so
a run that stops half way bills only for what you received.

The flat start charge is there because the cost of a run is dominated by loading
one search page, and that costs the same whether it yields 50 restaurants or 3.
Which means one thing is worth knowing: **the per-1,000 rate assumes you keep the
full 50.** Run with `maxItems: 10` and you are still buying a whole page load, so
your effective rate is nearer $1.05 per 1,000. Leave `maxItems` at 50 unless you
have a reason not to — the extra records are nearly free.

#### Empty results mean something

An empty dataset means the search genuinely matched no restaurants. Nothing else
produces one, so you can always trust an empty result to be an answer rather than
a breakage. Two different things keep that true, and they are worth telling apart
because your code has to read them differently.

**A problem with the input is a row, and the run still succeeds.** If there is
nothing to search for, or `city` names somewhere OpenTable does not list or
somewhere that could mean several places, the search is not run and the dataset
carries exactly one row — no restaurants beside it:

```json
[
  {
    "error": "place_ambiguous",
    "errorDescription": "OpenTable lists more than one place matching \"Brooklyn\" and nothing says which you mean: ...",
    "retrievedAt": "2026-08-26T09:00:00.000Z"
  }
]
```

The run finishes **SUCCEEDED** with the same reason in its status message, so
**a successful run is not by itself proof that a search happened** — check the
first row for an `error` key before treating the dataset as results. This is on
purpose: a run turning red is read by tooling as a broken Actor rather than as
bad input, and the row tells you far more than the status could. Error rows are
never charged.

The codes are:

| `error` | What happened | Worth retrying? |
|---|---|---|
| `bad_input` | Neither `term` nor `restaurantIds` was set | No — fix the input |
| `place_not_found` | OpenTable lists nowhere by that name | No — fix the input |
| `place_ambiguous` | The name matches several places; `errorDescription` lists them | No — qualify it with `state` |
| `place_unreadable` | OpenTable's own location directory would not load | **Yes** — this one is transient |

**A problem reading OpenTable still fails the run.** A refused request, an
expired session, a challenge, or a search page that has changed shape all fail
rather than finishing empty. So both channels are live: a red run means the
lookup broke, and an `error` row on a green run means the request could not be
made as asked.

Availability is a third, narrower case: when slots cannot be read, the
restaurant rows arrive as normal and carry `availabilityError` and
`availabilityErrorDescription` — there is no top-level `error` key on those rows,
and the restaurant data on them is good. See *Reading bookable slots*.

#### Notes and limits

- Availability is read three hours either side of your chosen time, in one request
  per batch of restaurants. There is no need to sweep several times of day.
- Slot booking tokens are signed and short-lived. Act on them promptly; do not
  store them as durable links.
- `databaseRegion` defaults to `NA`. If availability comes back empty for
  restaurants that clearly have tables outside North America, try `EMEA` or `APAC`.
- OpenTable refuses datacenter IP addresses, so this Actor runs through a proxy by
  default. If your plan carries neither the Unblocker nor residential proxies, runs
  will fail with a message saying so.
- **The search page is read as a race, not a queue.** Apify's Unblocker answers
  the page in 15-35 seconds when it answers at all, and spends a minute or more
  before admitting it will not — measured at about one attempt in three getting
  through on a bad day. So an attempt that has gone quiet for 35 seconds gets a
  second request sent alongside it rather than being waited out, up to ten
  attempts over about three minutes, and the first one to come back with the page
  wins. A healthy run still makes exactly one request. If every attempt fails at
  the proxy, the run fails with a message that says the proxy did not answer —
  that one is worth simply re-running.
- A run that cannot *read* what it was asked for fails rather than finishing empty,
  including when the search page changes shape. A run whose *input* cannot be used
  — no query, or a `city` that names nowhere or several places — succeeds and says
  so in a single `error` row instead. Between them, an empty dataset is always a
  real answer. See *Empty results mean something*.

### FAQ

**Is scraping OpenTable legal?**
This Actor reads publicly visible restaurant listings. It is unofficial and not
affiliated with OpenTable — check the platform's ToS and your own obligations
before using the data, and run it at a volume you are comfortable defending.

**Do I need an OpenTable account or API key?**
Not for restaurant records, ratings or the demand count — none of that needs a
session, cookie or key. Only bookable slot reading needs a browser session, and
you supply your own.

**What is `recentReservationCount` exactly?**
It is OpenTable's own "Booked N times today" figure, verified against the
rendered page. Two caveats: it does **not** reset at midnight (readings either
side of local midnight were 41/41, 12/12, 168/169, 100/101), so treat it as a
rolling trailing count; and it is only ever published for bookable restaurants,
so read `isBookable` before reading a `null` as a quiet restaurant.

**Why did my search return restaurants from the wrong state?**
It should not — that case is refused rather than guessed. A name matching
several places returns a single `place_ambiguous` row listing the choices, and
no restaurants. Qualify it with `state`: `Brooklyn, NY`, `Cambridge, MA`.

**Does it support pagination?**
`maxItems` controls how many restaurants come back, up to 50 from one search
page. Because the run's cost is dominated by that single page load, lowering
`maxItems` does not save much — leave it at 50 unless you have a reason not to.

**Why is availability empty even though the restaurant clearly has tables?**
Three things to check, in order: slot reading needs `sessionCookies` from your
own browser (they last about 40 minutes); it needs a **residential**
`availabilityProxy`, since OpenTable drops datacenter connections on that
endpoint before reading cookies; and outside North America try setting
`databaseRegion` to `EMEA` or `APAC`.

**How do I tell "fully booked" from "could not check"?**
Key off the *presence* of `available`, not its value. A restaurant that could
not be asked about has no `available`, `slots` or `slotCount` field at all — it
carries `availabilityError` instead, and its restaurant data is still good.

**Why did my run succeed but return no restaurants?**
Check the first row for an `error` key. A bad or ambiguous input finishes
SUCCEEDED with one explanatory row, because a red run reads to tooling as a
broken Actor rather than as bad input. A genuine failure to *read* OpenTable
does still fail the run.

# Actor input Schema

## `term` (type: `string`):

What to search OpenTable for — a restaurant name ("Balthazar") or a cuisine ("steak"). If you set "Restaurant IDs" below and leave this at its default, no search is run and only those IDs are read.

## `city` (type: `string`):

Where to search — a city, a town or a neighbourhood, written the way you would type it into OpenTable's own location box ("Miami", "Brooklyn", "Napa", "Sydney"). It is matched against OpenTable's own directories, so the search lands where OpenTable puts that place rather than where some other map does, and the run log names what it matched along with anything else that was close. Add a state or country the way OpenTable writes them when a name is ambiguous — "Cambridge, MA", "London, Canada". Needs no session or cookies. Leave it empty and set "Latitude"/"Longitude" instead if you would rather name the point yourself; give neither and OpenTable places the search by the address the proxy happens to use, which is not something a run controls.

## `state` (type: `string`):

Only needed when a city name is ambiguous. Leave it empty first: "Miami" and "London" resolve on their own, and on their own they search the whole market, which is usually what you want. If the run comes back saying several places share the name, put the state or country here exactly as OpenTable wrote it in that message — "NY", "MA", "IL", "Canada", "Portugal". You can also write it straight into "City" as "Manhattan, NY"; this field is the same thing with the comma spared.

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

Optional. Anchor the search on an exact point instead of naming a place in "City". Both this and "Longitude" have to be set for either to be used, and together they override "City". Nothing is assumed when they are left empty — there is no built-in default location.

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

See "Latitude". Coverage is global: OpenTable ranks by distance from this point wherever it is.

## `restaurantIds` (type: `array`):

Read these OpenTable restaurant IDs directly instead of (or as well as) searching. Each one is read from its own OpenTable page, so you get the full record — name, address, coordinates, rating, review count, price band, photos, whether it is bookable, and the recent-reservation count — without running a separate search per restaurant. This is the mode for tracking a known set of venues on a schedule, and it is charged at the ordinary per-restaurant price. Turn on "Read full details" as well if you also want the website, amenities and opening hours.

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

Cap on how many search results to keep. The search page returns 50 either way and the whole cost of a run is fetching it, so lowering this saves you almost nothing while raising it costs almost nothing — the per-restaurant price is the cheap part. Leave it at 50 unless you specifically want a short list.

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

Adds the restaurant's website, its full amenity list, opening hours, executive chef, dress code, parking, payment options, every cuisine, country, review counts, recent review text, popular dishes and special menus — everything that lives only on the restaurant's own OpenTable page. For search results this costs one extra page load PER RESTAURANT, where the search itself costs one load for fifty, so it is charged separately and is much slower. For "Restaurant IDs" that page is being read anyway, so this switch only decides whether you are charged for the extra fields.

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

Adds every open reservation time around "Time" for the given date and party size. REQUIRES "Session cookies" below — OpenTable answers this endpoint only for a browser session that is already valid. Turn this on without one and the run still succeeds and still returns every restaurant record; each row then carries "availabilityError" saying what was missing, and no slot fields at all, so a missing answer is never mistaken for a fully booked restaurant.

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

The day to read availability for, as YYYY-MM-DD. Required when "Read bookable slots" is on.

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

The time to centre the slot search on, as HH:MM (24-hour). Slots are returned for three hours either side, which covers a whole dinner service in one request.

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

Number of diners. Availability is per party size — a table for 2 and a table for 8 are different questions.

## `primeWindowStart` (type: `string`):

Start of the window scored by "primeWindowOpenPercent", as HH:MM. The default 18:00-21:00 is the dinner peak; move it to score a lunch service instead.

## `primeWindowEnd` (type: `string`):

End of the scored window, as HH:MM. Openness is normalised by each restaurant's own booking granularity, so a venue booking on the half hour is not scored as half empty against one booking on the quarter hour.

## `sessionCookies` (type: `string`):

REQUIRED for availability. OpenTable's reservation endpoint is behind Akamai and answers only a browser session that is already valid, so this Actor uses one you supply rather than manufacturing one. To get it: open opentable.com in your browser, open DevTools > Network, search any city, click a request to /dapi/fe/gql, and copy the whole "cookie" request header. A jar stays usable for roughly 40 minutes — but restaurants are batched 10 per request, so one jar covers thousands of them. Leave empty if you only want restaurant records.

## `userAgent` (type: `string`):

Must name the same browser the cookies came from. A mismatch between this and the client-hint headers is exactly what the bot filter looks for, so only change it if you also change "secChUa" to agree.

## `secChUa` (type: `string`):

The client-hint brand list your browser sends. This header is load-bearing: an otherwise identical request without it is refused. Copy it from the same DevTools request as the cookies if the default stops working.

## `secChUaPlatform` (type: `string`):

The platform your cookies were minted on, quoted — "Windows", "macOS" or "Linux". Should agree with "userAgent".

## `csrfToken` (type: `string`):

Optional. The x-csrf-token your browser sent alongside the cookies. Supply it if availability starts being refused with a valid-looking session.

## `databaseRegion` (type: `string`):

Which OpenTable regional backend to ask. "NA" covers North America and is the tested default; try "EMEA" or "APAC" if availability comes back empty for restaurants that clearly have tables.

## `includeRaw` (type: `boolean`):

Attaches OpenTable's untouched JSON under "raw" and "rawAvailability". Turn on when you need a field this Actor does not map yet.

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

Apify Proxy settings for the search page. OpenTable refuses datacenter exit IPs on its HTML with a 403, so the default asks for the UNBLOCKER group; switch to RESIDENTIAL if your plan carries it. A plan with neither quietly falls back to datacenter, and the run fails with a message saying which refusal it hit. The Unblocker itself is not always well: an attempt that goes quiet gets a second one sent alongside it, up to ten inside three minutes, so a healthy run makes exactly one request and a bad one keeps trying rather than waiting.

## `detailProxy` (type: `object`):

Proxy for reading individual restaurants' OpenTable pages, which — unlike the search page — are served to ordinary datacenter addresses. Leaving this at its default is what keeps reading by ID cheap. If profile pages start being refused the Actor falls back to "Proxy" above on its own; point this at the Unblocker only if that keeps happening.

## `availabilityProxy` (type: `object`):

Proxy for the reservation lookup specifically, which needs residential egress. Datacenter addresses are not just slower here, they are refused: the connection is dropped before your cookies are looked at. It also has to be separate from the main proxy, because the Unblocker that reads the search page carries GET requests only and answers this POST with a timeout and then a 502.

## Actor input object example

```json
{
  "term": "steak",
  "city": "Miami",
  "state": "NY",
  "restaurantIds": [],
  "maxItems": 50,
  "includeDetails": false,
  "includeAvailability": false,
  "time": "19:00",
  "partySize": 2,
  "primeWindowStart": "18:00",
  "primeWindowEnd": "21:00",
  "databaseRegion": "NA",
  "includeRaw": false,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "UNBLOCKER"
    ]
  },
  "detailProxy": {
    "useApifyProxy": true
  },
  "availabilityProxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `restaurants` (type: `string`):

One row per restaurant: neighbourhood, cuisine, price band, rating, review count and OpenTable's own recent-reservation count.

## `demand` (type: `string`):

How busy each restaurant is: the recent-reservation count, and how full the prime window is for runs that read bookable slots.

## `raw` (type: `string`):

Every field of every item, exactly as pushed.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

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

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("simple.actors/opentable-restaurants").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("simple.actors/opentable-restaurants").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call simple.actors/opentable-restaurants --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,simple.actors/opentable-restaurants"
        }
    }
}

```

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/vMZsVPDPWMNBuUEmj/builds/MtmVf7lxrkotkx8wF/openapi.json
