Arcadium Data Scrapers
Home / Guides / Google Hotel Reviews Scraper

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)

  1. Open arcadium/google-hotel-reviews and sign in to Apify (a free account works).
  2. Fill in the input form. The fields below are the ones the example uses.
  3. Set a maximum cost per run in the run options if you want a hard cap.
  4. Start the run. Rows appear in the Output tab as they arrive.
  5. Export the dataset as JSON, CSV or Excel, or connect an integration.
Input fieldTypeDescription
hotelsarrayHotels 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.
maxReviewsPerHotelintegerReviews 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+.
sortstringOrder of the reviews as Google sorts them. Newest first is needed for the date filter to stop early and for only-new-reviews mode.
reviewSourcestringGoogle'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.
languagestringInterface 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.
countrystringMarket 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
  }
}
FieldWhat it holds
rowTypeKind 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).
platformAlways 'googlehotels' (Google Hotels / Google Travel). The site a review was first posted on is in extras.source.
productNameName of the hotel as Google shows it.
productUrlGoogle Hotels page of the hotel (https://www.google.com/travel/hotels/entity/<token>).
entityIdGoogle's hotel entity token (Ch...). Pass it back as a hotel input to read the same hotel again.
reviewIdStable 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).
reviewUrlLink to the review on the partner site (Tripadvisor, Trip.com, ...) for partner reviews; null for Google reviews.
titleAlways null: Google hotel reviews have no headline.
textReview text as the reviewer wrote it (original language). Google translation in extras.textTranslated. Null for rating-only reviews.
prosAlways null on Google hotel reviews (shared review shape).
consAlways null on Google hotel reviews (shared review shape).
ratingOverallOverall rating on a 1 to 5 scale (partner ratings are scaled to 5; the raw value is in extras.rawRating and extras.rawScale).
ratingsAspect ratings the reviewer gave, 1 to 5, e.g. { rooms: 4, service: 2, location: 5 }. Empty object when none.
dateApproximate review date yyyy-mm-dd, computed from Google's relative date ('3 months ago'); precision in extras.datePrecision. Null if unreadable.
reviewerRoleAlways null on Google hotel reviews (shared review shape).
reviewerIndustryAlways null on Google hotel reviews (shared review shape).
reviewerCompanySizeAlways null on Google hotel reviews (shared review shape).
reviewerTimeUsedAlways null on Google hotel reviews (shared review shape).
switchedFromAlways null on Google hotel reviews (shared review shape).
alternativesConsideredAlways null on Google hotel reviews (shared review shape).
reasonForChoosingAlways null on Google hotel reviews (shared review shape).
reasonForSwitchingAlways null on Google hotel reviews (shared review shape).
adviceToOthersAlways null on Google hotel reviews (shared review shape).
incentivizedAlways null on Google hotel reviews (shared review shape).
incentiveAlways null on Google hotel reviews (shared review shape).
reviewerVerifiedAlways null on Google hotel reviews (shared review shape).
vendorResponseReply of the hotel (owner response) in its original language, names redacted. Translation in extras.ownerResponseTranslated, date in extras.ownerResponseDate.
helpfulVotesAlways null on Google hotel reviews (shared review shape).
languageAlways null: Google does not state the review language. A translated review has extras.translatedTo.
countryAlways null: Google shows no reviewer country. No personal data.
productHotel summary repeated on each row: { platform, entityId, name, url, rating, reviewCount, category, vendor, website }. rating and reviewCount are the Google figures.
extrasGoogle 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.
scrapedAtISO timestamp of when the row was collected.
statedCoverage rows: total the site states for the entity, null when it does not say.
deliveredCoverage rows: number of rows delivered for the entity in this run.
skippedSeenCoverage rows: items skipped because an earlier run already delivered them (only-new mode).
duplicatesCoverage rows: repeats the site returned that were dropped.
stopReasonCoverage 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).
capSourceCoverage rows: who limited the result, 'user' (your maximum or budget) or 'site' (a limit of the website). Null if nobody.
shortfallCoverage rows: true when fewer rows were delivered than the site states although no maximum or filter was set.
inputError and coverage rows: the input (URL, id or search text) the row is about. Example: 'https://www.example.com/page'.
codeError 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).
messageError rows: short human-readable explanation. Do not parse it, use code.
ratingHistogramHotel summary rows: share in percent and count per star, e.g. { "5": { "percent": 86, "count": 3931 } }.
topicsHotel summary rows: Google topic breakdown, one entry per topic: { name, shortName, mentions, positive, negative, sentiment (-1 to 1), polarity }.
partnerSitesHotel summary rows: partner review sites Google lists, { name, providerId, rating, ratingScale, reviewCount, url }.
ratingHotel summary rows: Google rating of the hotel, 1 to 5.
reviewCountHotel summary rows: number of Google reviews of the hotel.
hotelClassHotel summary rows: star class (1-5), null when Google gives none.
addressHotel summary rows: street address of the hotel as Google shows it.
countryCodeHotel 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.