kestrel actors docs

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

EventWhat it isPriceCharged for
reviewReview row$0.005One 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.

GoalInput
Latest 50 per restaurantmaxReviewsPerLocation: 50
Everything, oncemaxReviewsPerLocation: 0
Daily top-upmaxReviewsPerLocation: 0, sinceDate: "2 days"
Complaints onlymaxRating: 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.

FieldTypeDefaultWhat it does
startUrlsarray[]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).
locationIdsarray[]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.
restaurantNamesarray[]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.
locationTypestring (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.
languagesarray["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.
maxReviewsPerLocationinteger1000 = 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.
maxRatingnumber00 = 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.
tripTypesarray[]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.
sinceDatestring""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.
requireTextbooleanfalseDrop rating-only reviews before billing. TripAdvisor reviews almost always carry text, so this rarely removes much.
includeTranslatedbooleantrueEach 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.
includeLocationRowbooleantrueAlso 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.
sessionsinteger2How many residential proxy sessions (egress IPs) run in parallel. More is faster; each is paced separately. Range 1-16.
perIpnumber1Pace for each session. TripAdvisor serves 1 page/s per IP steadily; raising it causes rotations, not speed. Range 0.1-3.
proxyConfigurationobject{"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.

FieldTypeWhat it holds
typestringreview | location | status
targetstringThe input value (URL, id or name) this row came from
location_idstringTripAdvisor's numeric location id (the d<number> in its URLs)
geo_idstringTripAdvisor's numeric geo id of the city (the g<number> in its URLs)
location_namestringRestaurant or attraction name as the review page shows it
location_typestringrestaurant | attraction (what TripAdvisor's own placeType says the location is)
languagestringSite language the row was read in (en = tripadvisor.com, de = tripadvisor.de, …)
review_idstringTripAdvisor's numeric review id
original_languagestringLanguage the review was written in
translatedbooleantrue when the text is a machine translation into the site language
titlestringReview title
textstringFull review text
ratingnumber1-5 bubbles on review rows; the location's overall bubble rating (e.g. 4.3) on location rows
sub_ratingsobjectOn 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_datestringPublication date, YYYY-MM-DD
created_datestringDate the review was written, YYYY-MM-DD
visit_datestringMonth of the visit as TripAdvisor records it, YYYY-MM-DD
trip_typestringFAMILY | COUPLES | SOLO | BUSINESS | FRIENDS | null
tipsarrayThe diner's short tips for the next guest ("ask for the terrace"), when the review carries any
helpful_votesintegerHelpful votes the review received
reviewer_namestringReviewer display name
reviewer_usernamestringReviewer profile handle
reviewer_verifiedbooleanWhether TripAdvisor marks the reviewer as verified
reviewer_contributionsintegerReviewer's total contributions on TripAdvisor
reviewer_helpful_votesintegerHelpful votes across all of the reviewer's contributions
reviewer_hometownstringReviewer hometown as shown, e.g. "Brooklyn, New York"
photosarrayPhoto URLs attached to the review (1200 px renditions)
photo_countintegerHow many photos the review carries
responsestringThe owner's or manager's reply text, if any
response_datestringDate of the owner reply, YYYY-MM-DD
response_idstringTripAdvisor's id of the owner reply
urlstringDirect URL of the review (review rows) or of the location's review page (location rows)
location_urlstringThe location's canonical review page on the site the row was read from
review_countintegerTotal reviews the location has across all languages
rating_countsobjectReviews per bubble rating, keys "1"…"5"
reviews_by_languageobjectReviews per language, e.g. {"en": 4686, "pt": 2865}
rankingstringTripAdvisor ranking sentence, e.g. "#735 of 6,596 Restaurants in Lisbon"
ranking_positionintegerRank position in the city
ranking_ofintegerHow many locations of this kind are ranked in the city
ranking_categorystringWhat the ranking counts, e.g. "Restaurants in Lisbon" or "things to do in Lisbon"
cuisinesarrayCuisines TripAdvisor lists for the restaurant, e.g. ["Portuguese", "Seafood"]
price_rangestringTripAdvisor price band, e.g. "$$ - $$$"
category_tagsarrayAttraction categories, e.g. ["Points of Interest & Landmarks", "Cooking Classes"]
dish_tagsarrayThe dish and topic keywords TripAdvisor mines from the reviews ("clams", "tiger prawns", "worth the wait")
addressstringStreet address
citystringCity
statestringState or region, where TripAdvisor lists one
postal_codestringPostal code
country_codestringISO country code
latitudenumberLatitude
longitudenumberLongitude
phonestringPhone as listed
emailstringContact e-mail as listed
websitestringOfficial website as listed
menu_urlstringMenu link TripAdvisor points at
accepts_reservationsbooleanWhether TripAdvisor marks the restaurant as taking reservations
open_nowbooleanWhether TripAdvisor rendered the location as open at the time of the request
hoursarrayOpening hours as sentences, e.g. ["Sunday 12:00-23:00"]
permanently_closedbooleantrue when TripAdvisor marks the location as closed
descriptionstringThe location's own description, when TripAdvisor has one
neighborhoodsarrayNeighbourhood names TripAdvisor places the location in
reviews_listedintegerReviews the site lists for the location in this language (translations included)
reviews_fetchedintegerReviews read in this run for the location and language before filters
statusstringok | no_reviews | not_found | wrong_type | duplicate | error (status rows)
reviewsintegerReviews delivered for this location and language (status rows)
filteredintegerReviews dropped by the filters, never charged (status rows)
pagesintegerReview pages read (status rows)
totalintegerReviews the site lists in this language (status rows)
duplicatesinteger1 when this target resolved to a location another target already covers (status rows)
errorstringWhy a target failed, or what it turned out to be (status rows)
fetched_atstringUTC 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 perIp causes 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_Review target gets a free wrong_type status 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

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