How to use Google Hotel Reviews Scraper
Step by step: in the Apify Store, over the API, from JavaScript or Python, in n8n, and from AI agents through MCP or LangChain.
$0.54 per 1,000 reviews on the Free plan, down to $0.27 on higher plans (live Apify price on 2026-10-31).
1. In the Apify Store (no code)
- Open arcadium/google-hotel-reviews and sign in to Apify (a free account works).
- Fill in the input form. The fields below are the ones the example uses.
- Set a maximum cost per run in the run options if you want a hard cap.
- Start the run. Rows appear in the Output tab as they arrive.
- Export the dataset as JSON, CSV or Excel, or connect an integration.
| Input field | Type | Description |
|---|---|---|
hotels | array | Hotels to read: a Google Hotels or Google Travel link such as https://www.google.com/travel/hotels/entity/ChUImcGKtLfWgL9OGgkvbS8wNTZuODEQAQ, the entity token alone (the Ch... part, also the entityId of every row), a hotel name with its city such as "Hotel Adlon Kempinski Berlin", or a Google Maps place link (read by its name). Each hotel returns its reviews plus one free summary row. |
maxReviewsPerHotel | integer | Reviews to return for each hotel, 1 to 50,000, for example 500. Google pages up to 200 reviews per request; big hotels have 5,000+. |
sort | string | Order of the reviews as Google sorts them. Newest first is needed for the date filter to stop early and for only-new-reviews mode. |
reviewSource | string | Google's own reviews, the reviews of partner sites Google shows for the hotel (Tripadvisor, Trip.com, Expedia, ...), or both. Partner sites appear with a sample of their reviews and a link to each. |
language | string | Interface language as a code such as en, de, fr or pt-BR. Reviews in other languages come with Google's translation into this language in extras.textTranslated; the text field keeps the original. Dates are read for en, de, fr, es, it, pt and nl. |
country | string | Market as a two-letter country code, for example us, gb or de. It changes which partner sites Google lists. |
Example input:
{
"hotels": [
"https://www.google.com/travel/hotels/entity/ChUImcGKtLfWgL9OGgkvbS8wNTZuODEQAQ",
"Hotel Adlon Kempinski Berlin"
],
"maxReviewsPerHotel": 20,
"sort": "newest",
"reviewSource": "all",
"language": "en",
"country": "us"
}
2. Over the Apify API
The synchronous endpoint runs the actor and returns its rows in one call (for runs up to about five minutes; use the asynchronous run endpoint for longer ones).
curl -X POST "https://api.apify.com/v2/acts/arcadium~google-hotel-reviews/run-sync-get-dataset-items" \
-H "Authorization: Bearer $APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"hotels":["https://www.google.com/travel/hotels/entity/ChUImcGKtLfWgL9OGgkvbS8wNTZuODEQAQ","Hotel Adlon Kempinski Berlin"],"maxReviewsPerHotel":20,"sort":"newest","reviewSource":"all","language":"en","country":"us"}'
3. From JavaScript
// npm install apify-client
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('arcadium/google-hotel-reviews').call({
"hotels": [
"https://www.google.com/travel/hotels/entity/ChUImcGKtLfWgL9OGgkvbS8wNTZuODEQAQ",
"Hotel Adlon Kempinski Berlin"
],
"maxReviewsPerHotel": 20,
"sort": "newest",
"reviewSource": "all",
"language": "en",
"country": "us"
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);
4. From Python
# pip install apify-client
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("arcadium/google-hotel-reviews").call(run_input={
"hotels": ["https://www.google.com/travel/hotels/entity/ChUImcGKtLfWgL9OGgkvbS8wNTZuODEQAQ", "Hotel Adlon Kempinski Berlin"],
"maxReviewsPerHotel": 20,
"sort": "newest",
"reviewSource": "all",
"language": "en",
"country": "us",
})
items = client.dataset(run["defaultDatasetId"]).list_items().items
print(items)
5. In n8n
Paste this node into a workflow (it is a plain HTTP Request node). Create a Header Auth credential with name Authorization and value Bearer <APIFY_TOKEN>; each returned row becomes one item for the next node.
{
"nodes": [
{
"name": "Run arcadium/google-hotel-reviews",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [
0,
0
],
"parameters": {
"method": "POST",
"url": "https://api.apify.com/v2/acts/arcadium~google-hotel-reviews/run-sync-get-dataset-items",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "{\"hotels\":[\"https://www.google.com/travel/hotels/entity/ChUImcGKtLfWgL9OGgkvbS8wNTZuODEQAQ\",\"Hotel Adlon Kempinski Berlin\"],\"maxReviewsPerHotel\":20,\"sort\":\"newest\",\"reviewSource\":\"all\",\"language\":\"en\",\"country\":\"us\"}",
"options": {
"timeout": 300000
}
}
}
],
"connections": {}
}
6. From an AI assistant (MCP)
Add Apify’s MCP server to Claude Desktop, Cursor, VS Code or any MCP client, limited to this actor:
{
"mcpServers": {
"apify": {
"url": "https://mcp.apify.com/?tools=arcadium/google-hotel-reviews",
"headers": {
"Authorization": "Bearer <APIFY_TOKEN>"
}
}
}
}
More on the MCP page.
7. From LangChain
# pip install langchain-apify (reads APIFY_API_TOKEN from the environment)
from langchain_apify import ApifyActorsTool
tool = ApifyActorsTool("arcadium/google-hotel-reviews")
rows = tool.invoke({"run_input": {
"hotels": ["https://www.google.com/travel/hotels/entity/ChUImcGKtLfWgL9OGgkvbS8wNTZuODEQAQ", "Hotel Adlon Kempinski Berlin"],
"maxReviewsPerHotel": 20,
"sort": "newest",
"reviewSource": "all",
"language": "en",
"country": "us",
}})
# or give `tool` to an agent: create_react_agent(llm, [tool])
Sample output
One row, from the example in the actor’s README. Long text is shortened and lists are cut to two items.
{
"rowType": "review",
"platform": "googlehotels",
"productName": "Ritz Paris",
"entityId": "ChUImcGKtLfWgL9OGgkvbS8wNTZuODEQAQ",
"reviewId": "GoogleHotels___Ci9DQUlRQUNvZENodHljRjlvT25GSlZXbzJkV1Z0VTNwdmVGTXRVblUxY2s5aE1WRRAB",
"ratingOverall": 4,
"ratings": {
"rooms": 3,
"service": 4,
"location": 3
},
"date": "2026-10-16",
"text": "이곳은 아름다운스파 수영장 레스토랑 노을을 볼수있는곳 까지있어서 너무아름다워요 고급프랑스느낌",
"vendorResponse": "Cher [name],\n\nThank you for taking the time to share your wonderful feedback. ...\n\nKind regards,\n\n[name]\n Directrice d'hôtel, Ritz Paris",
"language": null,
"extras": {
"source": "Google",
"sourceType": "google",
"dateText": "6 days ago",
"datePrecision": "day",
"textTranslated": "This place is so beautiful, with a lovely spa, swimming pool, restaurant, and even a spot to watch the sunset. It has a luxurious French feel.",
"translatedTo": "en",
"tripType": "vacation",
"travellerType": "couple",
"visitMonth": "2026-10",
"highlights": [
"Luxury",
"Romantic"
],
"ownerResponseDate": "2026-10-17",
"photoCount": null
}
}
| Field | What it holds |
|---|---|
rowType | Kind of row. Filter on this first. Values: 'review': one review (charged). 'hotel-summary': one row per hotel with its Google rating, star histogram, topic breakdown and partner sites (free). 'coverage': stated total vs delivered and why reading stopped (free). 'error': an input could not be read; see code and message (free). |
platform | Always 'googlehotels' (Google Hotels / Google Travel). The site a review was first posted on is in extras.source. |
productName | Name of the hotel as Google shows it. |
productUrl | Google Hotels page of the hotel (https://www.google.com/travel/hotels/entity/<token>). |
entityId | Google's hotel entity token (Ch...). Pass it back as a hotel input to read the same hotel again. |
reviewId | Stable review id 'GoogleHotels___<Google post id>'. The same in every language; use it to deduplicate between runs. |
reviewIdSource | 'native' when the id comes from the site, 'derived' when it is a hash of the content (less stable). |
reviewUrl | Link to the review on the partner site (Tripadvisor, Trip.com, ...) for partner reviews; null for Google reviews. |
title | Always null: Google hotel reviews have no headline. |
text | Review text as the reviewer wrote it (original language). Google translation in extras.textTranslated. Null for rating-only reviews. |
pros | Always null on Google hotel reviews (shared review shape). |
cons | Always null on Google hotel reviews (shared review shape). |
ratingOverall | Overall rating on a 1 to 5 scale (partner ratings are scaled to 5; the raw value is in extras.rawRating and extras.rawScale). |
ratings | Aspect ratings the reviewer gave, 1 to 5, e.g. { rooms: 4, service: 2, location: 5 }. Empty object when none. |
date | Approximate review date yyyy-mm-dd, computed from Google's relative date ('3 months ago'); precision in extras.datePrecision. Null if unreadable. |
reviewerRole | Always null on Google hotel reviews (shared review shape). |
reviewerIndustry | Always null on Google hotel reviews (shared review shape). |
reviewerCompanySize | Always null on Google hotel reviews (shared review shape). |
reviewerTimeUsed | Always null on Google hotel reviews (shared review shape). |
switchedFrom | Always null on Google hotel reviews (shared review shape). |
alternativesConsidered | Always null on Google hotel reviews (shared review shape). |
reasonForChoosing | Always null on Google hotel reviews (shared review shape). |
reasonForSwitching | Always null on Google hotel reviews (shared review shape). |
adviceToOthers | Always null on Google hotel reviews (shared review shape). |
incentivized | Always null on Google hotel reviews (shared review shape). |
incentive | Always null on Google hotel reviews (shared review shape). |
reviewerVerified | Always null on Google hotel reviews (shared review shape). |
vendorResponse | Reply of the hotel (owner response) in its original language, names redacted. Translation in extras.ownerResponseTranslated, date in extras.ownerResponseDate. |
helpfulVotes | Always null on Google hotel reviews (shared review shape). |
language | Always null: Google does not state the review language. A translated review has extras.translatedTo. |
country | Always null: Google shows no reviewer country. No personal data. |
product | Hotel summary repeated on each row: { platform, entityId, name, url, rating, reviewCount, category, vendor, website }. rating and reviewCount are the Google figures. |
extras | Google hotel extras: source (Google, Tripadvisor, ...), sourceType (google/partner), dateText, datePrecision, edited, textTranslated, translatedTo, tripType (vacation/business), travellerType (solo/couple/family/friends), visitMonth, highlights, topicComments, ownerResponseTranslated, ownerResponseDate, ownerResponseDateText, photoCount, rawRating, rawScale. |
scrapedAt | ISO timestamp of when the row was collected. |
stated | Coverage rows: total the site states for the entity, null when it does not say. |
delivered | Coverage rows: number of rows delivered for the entity in this run. |
skippedSeen | Coverage rows: items skipped because an earlier run already delivered them (only-new mode). |
duplicates | Coverage rows: repeats the site returned that were dropped. |
stopReason | Coverage rows: why reading stopped. limit (your maximum reached), end (no more pages), dateFrom (older than your date), onlyNew (reached already delivered items), budget (run charge limit), cap (site-side depth limit), error (ended by a failure), filter (ended with a filter active). |
capSource | Coverage rows: who limited the result, 'user' (your maximum or budget) or 'site' (a limit of the website). Null if nobody. |
shortfall | Coverage rows: true when fewer rows were delivered than the site states although no maximum or filter was set. |
input | Error and coverage rows: the input (URL, id or search text) the row is about. Example: 'https://www.example.com/page'. |
code | Error rows: machine-readable reason. One of invalid_input (fix the input), not_found (page does not exist), blocked (site refused, retry later), unavailable (temporary error, retry later), filter_unsupported (the filter cannot be applied on this site), no_match (name search found nothing). |
message | Error rows: short human-readable explanation. Do not parse it, use code. |
ratingHistogram | Hotel summary rows: share in percent and count per star, e.g. { "5": { "percent": 86, "count": 3931 } }. |
topics | Hotel summary rows: Google topic breakdown, one entry per topic: { name, shortName, mentions, positive, negative, sentiment (-1 to 1), polarity }. |
partnerSites | Hotel summary rows: partner review sites Google lists, { name, providerId, rating, ratingScale, reviewCount, url }. |
rating | Hotel summary rows: Google rating of the hotel, 1 to 5. |
reviewCount | Hotel summary rows: number of Google reviews of the hotel. |
hotelClass | Hotel summary rows: star class (1-5), null when Google gives none. |
address | Hotel summary rows: street address of the hotel as Google shows it. |
countryCode | Hotel summary rows: country code of the hotel, null if unknown. |
Schedule it
In Apify Console, create a Task from your input and add a Schedule (for example daily). Each run writes a new dataset; integrations can push the rows to Google Sheets, Slack, a webhook or your database.