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

Search OpenTable anywhere in the world for each restaurant's full record — address, phone, coordinates, neighbourhood, cuisine, price band, rating, review count and photos — plus OpenTable's own recent-reservation count, a direct measure of how busy a restaurant is. Unofficial.

Pricing

from $0.30 / 1,000 restaurants

Rating

0.0

(0)

Developer

Simple Actors

Simple Actors

Maintained by Community

Actor stats

0

Bookmarked

1

Total users

1

Monthly active users

3 days ago

Last modified

Share

Search OpenTable for restaurants anywhere in the world and get the full record for each one — coordinates, address, phone, neighbourhood, cuisine, price band, rating, review count, photos, and OpenTable's own recent-reservation count, a direct measure of how busy a restaurant is.

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.

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.

What you get

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.

Input

{
"term": "steak",
"city": "Miami",
"maxItems": 20,
"includeAvailability": false
}

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.

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