OpenTable Scraper – Restaurants, Ratings & Demand avatar

OpenTable Scraper – Restaurants, Ratings & Demand

Pricing

from $0.30 / 1,000 restaurants

Go to Apify Store
OpenTable Scraper – Restaurants, Ratings & Demand

OpenTable Scraper – Restaurants, Ratings & Demand

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.

Pricing

from $0.30 / 1,000 restaurants

Rating

0.0

(0)

Developer

Simple Actors

Simple Actors

Maintained by Community

Actor stats

0

Bookmarked

3

Total users

3

Monthly active users

9 days ago

Last modified

Share

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 signalrecentReservationCount, 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 listedisBookable 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 coordinateslatitude 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

{
"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:

{ "restaurantIds": ["34009", "1470478", "7868"] }
FieldTypeDefaultWhat it does
termstringsteakRequired unless restaurantIds is set. What to search for.
citystringWhere to search. Matched against OpenTable's own directories.
statestringState or country, to disambiguate a city name.
latitude / longitudenumberAn exact point. Both or neither; overrides city.
restaurantIdsarray[]OpenTable IDs to read directly, skipping the search.
maxItemsinteger50Maximum restaurants to return.
includeDetailsbooleanfalseRead each restaurant's profile page for website, amenities, hours and more.
includeAvailabilitybooleanfalseRead bookable slots. Needs sessionCookies and a residential availabilityProxy.
date / time / partySizestring/int— / 19:00 / 2The reservation being checked.
primeWindowStart / primeWindowEndstring18:00 / 21:00The window scored by primeWindowOpenPercent.
sessionCookiesstringYour own browser session, for slot reading only.
userAgent, secChUa, secChUaPlatform, csrfTokenstringMust describe the same browser the cookies came from.
databaseRegionstringNANA, EMEA or APAC. Affects availability only.
includeRawbooleanfalseAttach OpenTable's untouched payloads.
proxyobjectUnblockerFor the search page.
detailProxyobjectApify proxyFor profile pages.
availabilityProxyobjectResidentialFor 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:

FieldExample
restaurantId1017331
nameBrooklyn Chop House - Downtown
urlhttps://www.opentable.com/r/brooklyn-chop-house-new-york
latitude / longitude40.7114624 / -74.0059409
address, city, state, postCode150 Nassau St, New York, NY, 10038
neighborhoodTriBeCa
cuisine, diningStyleSteakhouse, Fine Dining
priceBand, currencySymbol$31 to $50, $
rating, reviewCount4.1, 3045
recentReservationCount33
phone(212) 619-1200
isBookabletrue — see Bookable, or just listed? below
restaurantTypeGuestCenter (bookable) or Listing (directory entry)
permanentlyClosedtrue only where OpenTable says so
description, topReviewplain text, tags and entities resolved
photosarray 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:

FieldExample
websitehttp://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", …]
hoursOfOperationMon, Sun 7:00 am-10:00 pm / Tue-Thu 7:00 am-11:30 pm
executiveChefBrando De Oliveira
dressCodeSmart Casual
parkingInfoNone
paymentOptions["AMEX","Diners Club","Discover","JCB","Mastercard","Visa"]
cuisines["Italian","American"] — all of them, not just the primary
countryUnited States — the search record has no country field at all
totalReviewCount / textReviewCount745 / 10761
reviewsrecent review text
popularDishesdish name, description and how many reviews mention it
experiencesprix fixe menus, restaurant weeks and other special offers
publicTransit, crossStreet, cateringDetailsas 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:

{ "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:

FieldMeaning
availabletrue / false, or null if the restaurant was not in the answer
slotsevery open time, each with time, offsetMinutes and a booking token
slotCounthow many times are open in the three hours either side
bookingGranularityMinuteswhether this restaurant books on the quarter or half hour
primeWindowOpenSlots / primeWindowCapacity / primeWindowOpenPercenthow full the prime window is
noTimesReasonswhy 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 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 countryBrooklyn, 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

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:

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 researchincludeDetails 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.

EventPrice
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:

[
{
"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:

errorWhat happenedWorth retrying?
bad_inputNeither term nor restaurantIds was setNo — fix the input
place_not_foundOpenTable lists nowhere by that nameNo — fix the input
place_ambiguousThe name matches several places; errorDescription lists themNo — qualify it with state
place_unreadableOpenTable's own location directory would not loadYes — 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.