kestrel actors › Guest & customer reviews
TripAdvisor Restaurant Reviews Scraper — Diner Reviews
This TripAdvisor restaurant reviews scraper turns any restaurant or attraction page into structured rows: 1-5 bubbles, the full text, the diner's food / service / value / atmosphere sub-ratings, the visit date, the tips they left for the next guest, their photos and the owner's reply. Point it at tripadvisor restaurant reviews by URL, by location id or by name, pick your languages, and you get clean restaurant reviews and attraction reviews as a dataset, a CSV, an Excel file or a JSON API response. It is a restaurant reviews scraper built for restaurant groups, franchises, delivery platforms, POS and reputation vendors and city tourism boards — the people who need diner reviews at scale rather than one page at a time.
Run TripAdvisor Restaurant Reviews Scraper on Apify →
TripAdvisor restaurant reviews scraper pricing: what one row costs
| Event | What it is | Price | Charged for |
|---|---|---|---|
review | Review row | $0.005 | One TripAdvisor diner or visitor review: 1-5 bubbles, title, full text, the food/service/value/atmosphere sub-ratings, language and original language, visit date, trip type, the diner's tips, reviewer profile, photos and the owner's reply. Locations with no reviews, unknown ids, wrong-family ids and filtered reviews are free. |
maxReviewsPerLocation is the main cost control. 0 means every review the site lists for that location in that language; a number means the newest N. TripAdvisor lists 15 reviews per page on a restaurant and 10 on an attraction, newest first, and the actor reads the page-size straight off the page's own paging links — so no page is fetched twice and none is skipped.
Rough sizing for bulk restaurant reviews: a busy city-centre restaurant has 2,000-12,000 reviews in English alone, an attraction like a famous food market can pass 25,000, and each page is around a megabyte of residential proxy traffic. Start with 100-200 per location, look at the total column in the status rows, then decide.
| Goal | Input |
|---|---|
| Latest 50 per restaurant | maxReviewsPerLocation: 50 |
| Everything, once | maxReviewsPerLocation: 0 |
| Daily top-up | maxReviewsPerLocation: 0, sinceDate: "2 days" |
| Complaints only | maxRating: 2, requireText: true |
Billing is per delivered review row at $0.005. Locations with no reviews, unknown ids, wrong-family ids, duplicate targets and filtered-out reviews are all free, and the actor never delivers a row it could not charge for.
TripAdvisor restaurant reviews scraper input reference
Every field of TripAdvisor Restaurant Reviews, straight from the actor's input schema.
| Field | Type | Default | What it does |
|---|---|---|---|
startUrls | array | [] | Restaurant or attraction pages on any TripAdvisor domain, e.g. https://www.tripadvisor.com/Restaurant_Review-g189158-d2703366-Reviews-Cervejaria_Ramiro-Lisbon.html or an Attraction_Review URL. The geo id and slug do not have to be right, the numeric d<id> is what counts; ShowUserReviews deep links work too. Hotel URLs belong to the TripAdvisor Reviews Scraper (kestrel/tripadvisor-reviews-scraper). |
locationIds | array | [] | The numeric ids after d in TripAdvisor URLs, e.g. 2703366 or d2703366 — what an earlier run's location_id gives you. The fastest path: no lookup at all, and TripAdvisor resolves the id to the right page whether it is a restaurant or an attraction. |
restaurantNames | array | [] | Restaurant or attraction names, ideally with the city ("Cervejaria Ramiro Lisbon"). Resolved through TripAdvisor's own typeahead; a hit must share at least half of the words you typed, so a nonsense name comes back as an error row instead of a random bistro. The resolved name and type are in the location and status rows so you can check the match. |
locationType | string (enum) | "any" | What the targets must be: restaurant (Restaurant_Review pages), attraction (Attraction_Review pages) or any. A target that resolves to another family is reported as status = wrong_type and is never charged; hotels are always wrong_type here. One of: any, restaurant, attraction. |
languages | array | ["en"] | Which TripAdvisor sites to read — each site serves the reviews in its own language, machine-translated ones included: en (tripadvisor.com), en-gb, de, fr, es, it, pt (tripadvisor.com.br), nl, ja, zh, ru, sv, da, no, fi, pl, tr, el, ko, th, vi, id, cs, hu, he, en-au, en-ca, en-in, en-ie, en-sg. One location × one language = one job; the same review can appear once per language (as a translation), so pick the languages you need. |
maxReviewsPerLocation | integer | 100 | 0 = every review the site lists for the location in that language (15 per page on restaurants, 10 on attractions; busy restaurants run into the thousands, and each page is about a megabyte of residential proxy traffic). N = the newest N. The main cost control. Range 0-20000. |
maxRating | number | 0 | 0 = keep every review. 1-5 = keep only reviews with that many bubbles or fewer — 2 gives a complaints feed for a restaurant group. Filtered reviews are never charged (the pages are still read, so combine with a sensible max). Range 0-5. |
tripTypes | array | [] | Keep only reviews from these trip types: family, couples, solo, business, friends. Empty = all. Filtered reviews are never charged. TripAdvisor no longer publishes a meal type (breakfast/lunch/dinner) on restaurant reviews, so trip type is the visit context it does carry. |
sinceDate | string | "" | YYYY-MM-DD, or relative so schedules never go stale: "30 days", "2 weeks", "6 months". Pages come newest first, so the run stops reading once a whole page predates the date — a daily schedule reads one or two pages per restaurant. Empty = no date limit. |
requireText | boolean | false | Drop rating-only reviews before billing. TripAdvisor reviews almost always carry text, so this rarely removes much. |
includeTranslated | boolean | true | Each site shows reviews written in other languages as machine translations (rows carry translated = true and the original_language). Untick to keep only reviews written in the site's language — with several languages selected that also stops the same review arriving once per language. |
includeLocationRow | boolean | true | Also emit one row per location and language with its ids, name and type, bubble rating and review count, the count of reviews per rating and per language, the food/service/value/atmosphere sub-ratings, its ranking in the city, cuisines, price band, dish tags, address, coordinates, phone, hours and website. Never charged. |
sessions | integer | 2 | How many residential proxy sessions (egress IPs) run in parallel. More is faster; each is paced separately. Range 1-16. |
perIp | number | 1 | Pace for each session. TripAdvisor serves 1 page/s per IP steadily; raising it causes rotations, not speed. Range 0.1-3. |
proxyConfiguration | object | {"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]} | Apify Proxy with the RESIDENTIAL group is required: TripAdvisor's bot gate refuses every datacenter range outright, and only residential IPs are served. Keep the default. |
Example input
{
"startUrls": [
"https://www.tripadvisor.com/Restaurant_Review-g189158-d2703366-Reviews-Cervejaria_Ramiro-Lisbon_Lisbon_District_Central_Portugal.html",
"https://www.tripadvisor.com/Attraction_Review-g189158-d3214902-Reviews-Time_Out_Market_Lisboa-Lisbon.html"
],
"maxReviewsPerLocation": 200,
"languages": ["en"]
}
{
"locationIds": ["2703366", "d429386", "3214902"],
"locationType": "any",
"maxReviewsPerLocation": 100
}
TripAdvisor restaurant reviews scraper output fields
One row per diner or visitor review (charged), plus a free location context row and a free status row per location and language.
Dataset views in the Apify Console: Overview, Reviews, Complaints, Restaurants, Status.
| Field | Type | What it holds |
|---|---|---|
type | string | review | location | status |
target | string | The input value (URL, id or name) this row came from |
location_id | string | TripAdvisor's numeric location id (the d<number> in its URLs) |
geo_id | string | TripAdvisor's numeric geo id of the city (the g<number> in its URLs) |
location_name | string | Restaurant or attraction name as the review page shows it |
location_type | string | restaurant | attraction (what TripAdvisor's own placeType says the location is) |
language | string | Site language the row was read in (en = tripadvisor.com, de = tripadvisor.de, …) |
review_id | string | TripAdvisor's numeric review id |
original_language | string | Language the review was written in |
translated | boolean | true when the text is a machine translation into the site language |
title | string | Review title |
text | string | Full review text |
rating | number | 1-5 bubbles on review rows; the location's overall bubble rating (e.g. 4.3) on location rows |
sub_ratings | object | On a restaurant review, the bubbles the diner gave for food, service, value and atmosphere; on a location row the averages of the same four. Attractions carry none |
review_date | string | Publication date, YYYY-MM-DD |
created_date | string | Date the review was written, YYYY-MM-DD |
visit_date | string | Month of the visit as TripAdvisor records it, YYYY-MM-DD |
trip_type | string | FAMILY | COUPLES | SOLO | BUSINESS | FRIENDS | null |
tips | array | The diner's short tips for the next guest ("ask for the terrace"), when the review carries any |
helpful_votes | integer | Helpful votes the review received |
reviewer_name | string | Reviewer display name |
reviewer_username | string | Reviewer profile handle |
reviewer_verified | boolean | Whether TripAdvisor marks the reviewer as verified |
reviewer_contributions | integer | Reviewer's total contributions on TripAdvisor |
reviewer_helpful_votes | integer | Helpful votes across all of the reviewer's contributions |
reviewer_hometown | string | Reviewer hometown as shown, e.g. "Brooklyn, New York" |
photos | array | Photo URLs attached to the review (1200 px renditions) |
photo_count | integer | How many photos the review carries |
response | string | The owner's or manager's reply text, if any |
response_date | string | Date of the owner reply, YYYY-MM-DD |
response_id | string | TripAdvisor's id of the owner reply |
url | string | Direct URL of the review (review rows) or of the location's review page (location rows) |
location_url | string | The location's canonical review page on the site the row was read from |
review_count | integer | Total reviews the location has across all languages |
rating_counts | object | Reviews per bubble rating, keys "1"…"5" |
reviews_by_language | object | Reviews per language, e.g. {"en": 4686, "pt": 2865} |
ranking | string | TripAdvisor ranking sentence, e.g. "#735 of 6,596 Restaurants in Lisbon" |
ranking_position | integer | Rank position in the city |
ranking_of | integer | How many locations of this kind are ranked in the city |
ranking_category | string | What the ranking counts, e.g. "Restaurants in Lisbon" or "things to do in Lisbon" |
cuisines | array | Cuisines TripAdvisor lists for the restaurant, e.g. ["Portuguese", "Seafood"] |
price_range | string | TripAdvisor price band, e.g. "$$ - $$$" |
category_tags | array | Attraction categories, e.g. ["Points of Interest & Landmarks", "Cooking Classes"] |
dish_tags | array | The dish and topic keywords TripAdvisor mines from the reviews ("clams", "tiger prawns", "worth the wait") |
address | string | Street address |
city | string | City |
state | string | State or region, where TripAdvisor lists one |
postal_code | string | Postal code |
country_code | string | ISO country code |
latitude | number | Latitude |
longitude | number | Longitude |
phone | string | Phone as listed |
email | string | Contact e-mail as listed |
website | string | Official website as listed |
menu_url | string | Menu link TripAdvisor points at |
accepts_reservations | boolean | Whether TripAdvisor marks the restaurant as taking reservations |
open_now | boolean | Whether TripAdvisor rendered the location as open at the time of the request |
hours | array | Opening hours as sentences, e.g. ["Sunday 12:00-23:00"] |
permanently_closed | boolean | true when TripAdvisor marks the location as closed |
description | string | The location's own description, when TripAdvisor has one |
neighborhoods | array | Neighbourhood names TripAdvisor places the location in |
reviews_listed | integer | Reviews the site lists for the location in this language (translations included) |
reviews_fetched | integer | Reviews read in this run for the location and language before filters |
status | string | ok | no_reviews | not_found | wrong_type | duplicate | error (status rows) |
reviews | integer | Reviews delivered for this location and language (status rows) |
filtered | integer | Reviews dropped by the filters, never charged (status rows) |
pages | integer | Review pages read (status rows) |
total | integer | Reviews the site lists in this language (status rows) |
duplicates | integer | 1 when this target resolved to a location another target already covers (status rows) |
error | string | Why a target failed, or what it turned out to be (status rows) |
fetched_at | string | UTC timestamp of the request |
TripAdvisor restaurant reviews scraper in Python, JavaScript and curl
Python
from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("kestrel/tripadvisor-restaurant-reviews").call(run_input={
"locationIds": ["2703366"],
"locationType": "restaurant",
"languages": ["en"],
"maxReviewsPerLocation": 200,
"maxRating": 3,
"requireText": True,
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
if row["type"] != "review":
continue
subs = row["sub_ratings"] or {}
print(row["rating"], subs.get("food"), subs.get("service"), row["review_date"], row["title"])
JavaScript (Node)
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('kestrel/tripadvisor-restaurant-reviews').call({
startUrls: ['https://www.tripadvisor.com/Restaurant_Review-g35805-d429386-Reviews-The_Cheesecake_Factory-Chicago_Illinois.html'],
maxReviewsPerLocation: 100,
sinceDate: '90 days',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
const reviews = items.filter((i) => i.type === 'review');
console.log(reviews.length, 'reviews,', reviews.filter((r) => r.response).length, 'answered by the owner');
curl
curl -X POST "https://api.apify.com/v2/acts/kestrel~tripadvisor-restaurant-reviews/run-sync-get-dataset-items?token=$APIFY_TOKEN&format=csv" \
-H 'Content-Type: application/json' \
-d '{"locationIds":["2703366"],"maxReviewsPerLocation":50,"languages":["en"]}' \
-o restaurant-reviews.csv
The same three calls are runnable files in the examples repository: examples/tripadvisor-restaurant-reviews.py|.js|.sh.
Limits and honest notes
- Residential proxy is not optional. TripAdvisor fingerprints the TLS handshake: every datacenter range is refused outright and every Chrome-shaped client is challenged. Raising
perIpcauses rotations, not speed. - Paging is 15 reviews per page on a restaurant and 10 on an attraction, read off the page's own paging links so no page is fetched twice and none is skipped.
- Restaurants and attractions only. A
Hotel_Reviewtarget gets a freewrong_typestatus row naming what it actually is, and points at the hotel sibling. - Pages are heavy. A busy city-centre restaurant has 2,000-12,000 reviews in English alone, an attraction like a famous food market can pass 25,000, and each page is around a megabyte of residential proxy traffic.
FAQ
How much does it cost to scrape 10,000 restaurant reviews?
$50 in event charges — 10,000 rows at $0.005 — plus Apify platform usage for the compute and the residential traffic. Filtered reviews are free, so a maxRating: 2 run over the same restaurants costs a fraction of that because only the rows you keep are billed.
Does it get every review, or only the first page?
Every one, if you ask for it. maxReviewsPerLocation: 0 walks the whole list in steps of 15 (restaurants) or 10 (attractions) until TripAdvisor stops returning new ids. The total field in the status row tells you how many that domain lists, and reviews_listed on the location row repeats it, so you can always confirm you got them all.
Can I scrape attraction reviews with the same run?
Yes. Attractions are first-class here: set locationType to attraction or leave it at any and mix restaurant and attraction targets freely. Tripadvisor attraction reviews come back with the same fields minus sub_ratings (attractions have none) and plus category_tags on the location row.
What happens if I pass a hotel URL or a hotel id?
A hotel URL is rejected up front with a message pointing at kestrel/tripadvisor-reviews-scraper. A hotel id cannot be recognised before it is fetched, so the actor reads one page, sees that it is a hotel, emits a free wrong_type status row naming the sibling actor, and stops. You are never charged for it and it is never scraped here.
How fresh are the reviews?
They are read live at run time; fetched_at on every row is the UTC timestamp of the request. There is no cache and no stored snapshot, so a review published a minute ago appears in the next run.
Can I get restaurant reviews with photos?
Yes — photos is an array of 1200 px image URLs and photo_count is the number of them. Roughly one review in five carries at least one photo, and food photos cluster in the reviews that also mention dishes.
Does it handle restaurants that reply to reviews?
That is one of the strongest fields in the output. response, response_date and response_id carry the owner's or manager's reply. Some chains reply to essentially every review, which makes them a clean benchmark for how fast your own locations answer.
Why do the review counts differ between TripAdvisor domains?
Because each domain lists a different set: natively written reviews in that language plus the machine translations TripAdvisor has produced for it. review_count on the location row is the global total across all languages; total in the status row and reviews_listed on the location row are what that one domain lists. reviews_by_language breaks the global number down per language so you can plan which domains to read.
Can I run this on a schedule?
Yes — use Apify Schedules with a relative sinceDate so the input never goes stale, and the run reads only the new pages. Pair it with a webhook to push new rows into your warehouse as they land. This is the normal setup for ongoing review monitoring and it is cheap, because a top-up run reads one or two pages per location.
Is there a limit on how many locations per run?
No hard limit. Practically, keep a run under an hour: with two sessions at one request per second, that is a few thousand pages. For very large fleets, split the list across several runs — they are independent, and location_id makes the results trivial to merge.
What does status: "no_reviews" mean?
The location exists and was read, but the domain you asked for lists no reviews for it — a brand-new restaurant, or a language nobody has reviewed it in. It is a free row, and it is deliberately distinct from error, which means the page could not be read at all.
Can I use the output commercially?
The actor is a tool; what you may do with the data depends on your use case, your jurisdiction and TripAdvisor's terms. Analysis, benchmarking and internal reporting are the normal uses. Verbatim republication of review text and retention of reviewer identities are the two areas where you should take your own legal advice — see the legality section above.
How to use TripAdvisor Restaurant Reviews Scraper
Related scrapers
Run TripAdvisor Restaurant Reviews Scraper
The actor lives on the Apify Store, where the pricing tab carries the live per-row price and the input form has every field above: apify.com/kestrel/tripadvisor-restaurant-reviews