# Google Hotels Scraper — Prices by OTA, Calendar & Reviews (`oswaldocarabano/google-hotels-prices-scraper`) Actor

Scrape Google Hotels: hotels of any search with price, rating, sentiment by aspect, amenities and photos; the full price ladder across Booking, Expedia, Agoda and the official site with free-cancellation dates; nightly price calendars; review texts. No login. Empty or failed rows are never charged.

- **URL**: https://apify.com/oswaldocarabano/google-hotels-prices-scraper.md
- **Developed by:** [Oswaldo Carabano](https://apify.com/oswaldocarabano) (community)
- **Categories:** Travel, E-commerce
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 hotels

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Google Hotels Scraper — prices by booking site, price calendar and reviews

**Scrape Google Hotels for any city, any dates and any number of guests.** Get every
hotel of a search with its price, rating, review breakdown, sentiment by aspect,
amenities, photos and nearby places — or go one level deeper and get the **full price
ladder across booking sites** (Booking.com, Expedia, Hotels.com, Agoda, Trip.com, the
hotel's official website and more), a **nightly price calendar**, or **review texts**.

**No login. No cookies. No browser.** From **$2.00 per 1,000 hotels**. Rows that fail,
come back empty or repeat are **never charged**.

***

### What you can use it for

| If you are… | Use | You get |
|---|---|---|
| A **revenue manager** or hotel owner tracking rate parity | `hotel-prices` | Your official-site price next to every OTA, the % gap to the cheapest offer, free-cancellation deadlines |
| A **travel startup** or metasearch building a hotel database | `hotel` | Up to ~160 unique hotels per city with coordinates, class, rating, amenities and photos |
| An **analyst** studying seasonality or event pricing | `price-calendar-day` | The cheapest, official and highest price for each check-in date of a range |
| A **reputation** or market-research team | `review` | Review texts from Google, Tripadvisor, Trip.com and more, with scores and hotel replies |
| Anyone comparing **cities, dates or group sizes** | any | Dates, currency, adults and children ages go into the Google request itself, so prices are real for that stay |

***

### Four outputs, one per run

Pick `outputType`. Every run produces one kind of row, so your exports stay clean.

| `outputType` | One row per | Price | What is inside |
|---|---|---:|---|
| `hotel` *(default)* | hotel of a search | **$0.002** | name, ids, coordinates, address (when shown), class, rating, review histogram, sub-scores, sentiment by aspect, featured price with and without taxes, total for the stay, deal badge, check-in/out times, website, description, amenities, photos, nearby places with travel times |
| `hotel-prices` | hotel | **$0.0035** | everything above **plus every booking-site offer** (median **23** per hotel) with nightly and total prices, with and without taxes, free-cancellation date, member deals, official-site price and parity, address, hotel phone, AI review summary |
| `price-calendar-day` | hotel × check-in date | **$0.003** | lowest, official and highest price for that night, every offer, free-cancellation count |
| `review` | review | **$0.001** | full text, score and scale, source site, approximate date, hotel reply, per-aspect scores, highlight tags |

Actor start: **$0.00001** (platform minimum). Error rows, duplicates, hotels without any
priced offer, sold-out nights and reviews without text: **$0**.

***

### Measured, not promised

Every number on this page comes from our own measurements against the live site in
October 2026, with its sample size. **Fields with a measured fill rate of 0% are not
listed.**

#### Hotel rows — n = 706 hotels, 20 cities, 14 countries (13 Oct 2026)

Populated fields per hotel: **median 27 of 28** (minimum 17, 90th percentile 28).

| Field | Filled |
|---|---:|
| name, ids, coordinates, rating, review count, rating histogram, photos, nearby places, sub-scores, country | **100%** |
| price, price with taxes, total for the stay, currency | 99.7% |
| amenities | 96.7% |
| official website | 95.9% |
| sentiment by aspect (6–32 aspects per hotel) | 92.5% |
| hotel class (stars) | 86.5% (Germany 25%: Google only shows official classes there) |
| description | 82.2% |
| check-in / check-out time | 78.0% |
| deal badge ("23% less than usual") | 23.4% |

#### Price ladders — n = 36 hotel pages in 6 countries (13 Oct) + 60 in 3 countries (14 Oct 2026)

- Offers with a price per hotel: **median 23**, 90th percentile 30, maximum 42.
- Total price for the stay on **1,205 of 1,205** offers.
- At least one offer with **free cancellation** on 48 of 60 hotels, with the deadline date.
- Address on 100% of hotel pages, hotel phone on 29 of 30, AI review summary on 37 of 60.
- Official website among the offers on 46 of 60 hotels; member-only deals on 24 of 60.
- The headline search price was the cheapest offer only **20 times out of 36** — the
  ladder tells you more than the headline.

#### 50 test runs on Apify — 14 Oct 2026

- **50 of 50 runs succeeded**: 23 Google markets, 16 languages, all four outputs, filters,
  hotel links, sizes from 1 to 510 rows, and bad inputs (which end green with an
  explanatory error row that is not charged).
- **Charged = delivered in 50 of 50 runs** (3,608 rows); **0 duplicate rows**.
- Default input `{}`: 20 hotels in 7–11 seconds (6 runs, including 2 on the current build). Slowest run: a 31-night calendar for 10 hotels (310 hotel-nights, 285 with prices) in 5 min 12 s (5 min 48 s on the current build, which caps requests in flight at `maxConcurrency`).
- Non-empty fields per row (all fields, including the dates and guests you asked for):
  hotel **median 46** (n = 1,857) · hotel-prices **61** (n = 120) ·
  price-calendar-day **29** (n = 451) · review **19** (n = 1,180).
- The same hotel keeps the same `entity` id between runs (58 of 58 hotels seen in two
  identical runs had the same name; coordinates within 10 m).

#### Reviews — n = 20 hotels × 3 pages = 600 reviews (13 Oct 2026)

- **100%** with text. 10 per request, paginated until your limit.
- Sources seen: Google, Tripadvisor, Priceline, Trip.com, IHG, hotels' own sites.
- **Reviewer names, profile links and avatars are never collected.**

***

### How it compares

Figures read from the public Apify Store API on **13–14 Oct 2026** (users and run outcomes
over the last 30 days) and from each actor's published input and output schemas. We did
not run anyone else's actor.

| | **This actor** | Most-used alternative | Second most-used | Most complete alternative |
|---|---|---|---|---|
| Users, last 30 days | new | 520 | 517 | 47 |
| Failed or timed-out runs, last 30 days | **0 of 50** varied test runs (14 Oct 2026, 23 markets, all four outputs) | **43.6%** of 42,351 | 5.3% | 0.3% |
| Price per hotel row | **$0.002** | $0.00499 | $0.001 + **$0.005 per run start** | $0.004 + $0.002 per offer |
| Hotels per run | many cities × up to 300 hotels | many | **one** | many |
| Price ladder by booking site | ✅ median 23 offers, one charge | — | — | ✅ charged per offer |
| Price calendar | ✅ up to 31 dates × many hotels | — | ✅ one hotel | ✅ |
| Review texts | ✅ | — | — | ✅ |
| Sentiment by aspect | ✅ 92.5% | — | — | declared |
| Free-cancellation deadline per offer | ✅ | — | — | declared |
| Hotel phone and address | ✅ (hotel-prices) | — | — | declared |
| Output fields declared in its schema | 98 | 20 | 0 | 70 |
| Non-empty fields per hotel row, **measured** | **median 46** (n = 1,857 rows) | not published | not published | not published |
| Unknown city charged? | **no**: rows are charged only when the place Google searched matches what you typed; a guessed city is an error row, $0 (0 of 120 made-up names charged, 14 Oct 2026) | not stated | not stated | not stated |
| Duplicates across pages charged? | **no** (deduplicated by hotel id) | not stated | n/a | not stated |

***

### Input

Everything has a default: an empty input returns 20 hotels in Lisbon for a 2-night stay
two weeks from today, in under 10 seconds (7–11 s in 6 runs on 14 Oct 2026).

| Field | Default | Notes |
|---|---|---|
| `outputType` | `hotel` | `hotel`, `hotel-prices`, `price-calendar-day`, `review` |
| `locations` | `["Lisbon"]` | as you would type them in Google Hotels; "Valencia, Spain" beats "Valencia". Type a city or neighbourhood, not a landmark (see *Wrong-city protection*) |
| `hotelUrls` | `[]` | Google Hotels links with `/travel/hotels/entity/<id>`, or the id; only the id is read |
| `checkIn` / `checkOut` / `nights` | today + 14 / — / 2 | prices are for exactly this stay |
| `adults` / `childrenAges` | 2 / `[]` | going from 2 to 4 adults changed the price in 17 of 18 hotels we tested |
| `currency` / `language` / `country` | USD / en / us | currency is set inside the request, never converted by us |
| `maxHotels` | 20 | unique hotels per location (about 160 available for a big city) |
| `stars`, `minRating`, `maxPrice` | any | applied by Google itself; in our test 100% of returned hotels matched |
| `amenities` | `[]` | all must be present; "Pool", "Parking", "Wi-Fi", "Breakfast", "Kitchen" accept any variant |
| `sortBy` | relevance | lowest/highest price, highest rating, most reviewed (sorted per location) |
| `includeVacationRentals` | false | Google mixes them into ~10% of results |
| `calendarDays` | 7 | check-in dates for `price-calendar-day` (1–31) |
| `maxReviewsPerHotel` | 30 | for `review` |
| `maxConcurrency` | 6 | requests in flight for the whole run (searches, hotel pages, calendar dates and reviews); 6 is the measured sweet spot at 512 MB |

#### Example: rate parity for 3 cities

```json
{
  "outputType": "hotel-prices",
  "locations": ["Madrid", "Barcelona", "Valencia, Spain"],
  "checkIn": "2026-12-15",
  "nights": 3,
  "adults": 2,
  "currency": "EUR",
  "language": "es",
  "country": "es",
  "maxHotels": 30,
  "stars": ["4", "5"]
}
```

***

### Output example (`hotel-prices`, shortened)

```json
{
  "_type": "hotel-prices",
  "name": "Embassy Suites by Hilton Miami International Airport",
  "entity": "ChoI7J2s6622-ausARoNL2cvMTFiN2NzZnA2axAB",
  "address": "3974 NW S River Dr, Miami, FL 33142",
  "stars": 4, "rating": 3.6, "reviewCount": 6109,
  "currency": "USD",
  "offerCount": 28,
  "lowestPrice": 106.7, "lowestPriceSource": "KAYAK.com",
  "officialPrice": 153.16, "officialSource": "Embassy Suites by Hilton Miami International Airport",
  "officialVsLowestPct": 43.5,
  "freeCancellationOfferCount": 0,
  "offers": [
    {
      "source": "Embassy Suites by Hilton Miami International Airport", "isOfficial": true,
      "price": "$153", "priceValue": 153.16, "priceWithTaxes": "$173", "priceWithTaxesValue": 173.08,
      "totalPrice": "$306", "totalPriceWithTaxes": "$346",
      "freeCancellation": false, "freeCancellationUntil": null
    }
  ],
  "checkIn": "2026-11-18", "checkOut": "2026-11-20", "nights": 2, "adults": 2
}
```

Error rows look like this and are never charged:

```json
{ "_type": "error", "error": "city-not-found", "message": "Google Hotels did not recognise \"zxqvwk\" as a place… This row is not charged.", "location": "zxqvwk", "cityResolved": null, "placeCheck": "no-place" }
```

#### Wrong-city protection

For text it does not recognise, Google Hotels sometimes **guesses a real city** and
returns its hotels anyway — "Qwzxkpqville Zzrt" came back as New York, "Blorptastic
Vrumm" as Vilnius, depending on the exit IP. Those rows would look perfect and be wrong.
So a search is delivered and charged **only when the place Google searched
(`cityResolved`) is a name of what you typed**:

- the same name, ignoring accents, case and "St."/"Saint" ("Medellin" → Medellín,
  "Shibuya, Tokyo" → Shibuya), or
- a known alternative name of the same city — other languages and scripts, and common
  abbreviations ("NYC" → New York, "Köln" → Cologne, "CDMX" → Ciudad de México,
  "Den Haag" → The Hague, "Bangkok" → กรุงเทพมหานคร), from GeoNames (CC BY 4.0), or
  from Wikidata for smaller places.

Otherwise you get one `city-not-found` row, **not charged**, with `cityResolved` (what
Google answered) and `placeCheck` (why it was refused: `place-mismatch` for a guessed
city, `place-unconfirmed` when the name check could not be completed). Never charged on
doubt. Measured on 14 Oct 2026 from three different exit routes: **0 of 120** searches for
24 made-up names charged, and **159 of 162** searches for 54 real cities in 20 languages
accepted. The 3 refused were "near Eiffel Tower", which Google maps to all of Paris: for
landmarks, type the city.

The run also writes `ERRORS` (every error, always present) and `RUN_SUMMARY` (delivered,
charged, duplicates, errors) to its key-value store.

***

### Reliability

- **Retries that change the exit IP**, never hammer the same one: a throttled or refused
  request moves to a new session, then to another route, then waits (60 s and up, bounded
  by your run timeout), and only then writes an explanatory error row — not charged.
- **The run finishes before its timeout** and says what was left, instead of being cut.
- **Resumable**: if the platform migrates the run, nothing is delivered or charged twice.
- **If Google changes its page format**, the affected rows become `schema-changed` error
  rows and the run stays green; it does not guess.
- **Stops cleanly** at the maximum cost you set for the run.

### FAQ

**Is there a limit of hotels per city?** Google returns about 18 hotels per page and
roughly 160 unique hotels in 10 pages for a large city. `maxHotels` goes up to 300; you
get what Google lists.

**Why is the cheapest offer different from the featured price?** Google's featured price
is not always the cheapest site. That is exactly what `hotel-prices` shows.

**Do I need a proxy?** No. The actor handles its own routes.

**Are duplicates charged between runs?** Deduplication is inside a run (by hotel id). Two
separate runs over the same city deliver and charge each run's hotels.

**Do you collect reviewer names?** No: not the name, not the profile link, not the photo.

**What is not included?** Room-type names per offer and sponsored ads: they were empty in
our samples (0 of 1,205 offers), so we do not promise them. The hotel phone and address
come from the hotel page, so they are on `hotel-prices` rows, not on `hotel` rows. Rows from `hotelUrls` take their amenities from the hotel page, which names fewer of them (Wi-Fi and pools as generic "Wi-Fi" and "Pool"); the raw codes are always in `amenityCodes`.

# Actor input Schema

## `outputType` (type: `string`):

One row type per run. <b>hotel</b>: every hotel of a search with its featured price, rating, reviews breakdown, sentiment by aspect, amenities, photos and nearby places ($2 per 1,000). <b>hotel-prices</b>: the full price ladder of each hotel across booking sites (median 23 offers) with official-site parity and free-cancellation dates. <b>price-calendar-day</b>: the price ladder for each check-in date of a range. <b>review</b>: review texts with score, source and hotel replies.

## `locations` (type: `array`):

Cities, neighbourhoods or areas as you would type them in Google Hotels ("Lisbon", "Shibuya, Tokyo", "Valencia, Spain"). Each location is searched with your dates and filters. Rows are delivered and charged only when the place Google searched (cityResolved) is a name of what you typed, including other languages and abbreviations ("NYC", "Köln", "CDMX"). If Google does not recognise the text or guesses a different city, you get one error row that is not charged. Type a city or neighbourhood, not a landmark.

## `hotelUrls` (type: `array`):

Specific hotels: Google Hotels links containing /travel/hotels/entity/\<id> or the id itself. Used together with, or instead of, locations. Nothing else is fetched from these links: only the hotel id is read.

## `checkIn` (type: `string`):

YYYY-MM-DD. Empty = 14 days from today. Prices change with dates, so every row carries the dates it was priced for.

## `checkOut` (type: `string`):

YYYY-MM-DD. Empty = check-in plus "Nights".

## `nights` (type: `integer`):

Length of the stay when no check-out date is given. Also the stay length of each night in the price calendar.

## `adults` (type: `integer`):

Guests change the price: going from 2 to 4 adults changed the price in 17 of 18 hotels we tested.

## `childrenAges` (type: `array`):

One age (0–17) per child, for example \[6, 10]. Empty = no children.

## `currency` (type: `string`):

Prices are returned in this currency (it is set inside the Google Hotels request, not converted by us).

## `language` (type: `string`):

Language of descriptions, labels and reviews as Google shows them.

## `country` (type: `string`):

Google Hotels market (booking sites and prices can differ by market).

## `maxHotels` (type: `integer`):

Unique hotels per location (duplicates across pages are removed and never charged). About 18 hotels per results page; Google gives roughly 160 unique hotels in 10 pages for a big city.

## `stars` (type: `array`):

Only hotels of these classes. Applied by Google itself (tested: 100% of returned hotels match).

## `minRating` (type: `string`):

Applied by Google itself (tested on 5 cities: every returned hotel meets it).

## `maxPrice` (type: `integer`):

In the chosen currency, applied by Google itself. Empty = no limit.

## `amenities` (type: `array`):

Keep only hotels that have ALL of these. "Pool", "Parking", "Wi-Fi", "Breakfast" and "Kitchen" accept any variant (free or paid, indoor or outdoor). Filtered after reading the results, so it can take more pages to reach "Max hotels".

## `sortBy` (type: `string`):

Relevance = Google's order. The other options sort the hotels of each location before delivering them (hotel rows only).

## `includeVacationRentals` (type: `boolean`):

Google mixes vacation rentals into some results (10% of rows in our 20-city sample). Off = hotels only.

## `calendarDays` (type: `integer`):

For price-calendar-day: one row per hotel and check-in date, starting at the check-in date. Each date is one Google request, so 30 dates × 20 hotels = 600 rows.

## `maxReviewsPerHotel` (type: `integer`):

For review rows. Reviews come 10 per request; reviews without text are skipped and not charged.

## `maxConcurrency` (type: `integer`):

Requests in flight at once. 6 is the measured sweet spot at 512 MB; 10 is the maximum. Lower values do not improve success, they only make the run slower.

## Actor input object example

```json
{
  "outputType": "hotel",
  "locations": [
    "Lisbon"
  ],
  "hotelUrls": [],
  "checkIn": "",
  "checkOut": "",
  "nights": 2,
  "adults": 2,
  "childrenAges": [],
  "currency": "USD",
  "language": "en",
  "country": "us",
  "maxHotels": 20,
  "stars": [],
  "minRating": "any",
  "amenities": [],
  "sortBy": "relevance",
  "includeVacationRentals": false,
  "calendarDays": 7,
  "maxReviewsPerHotel": 30,
  "maxConcurrency": 6
}
```

# Actor output Schema

## `results` (type: `string`):

The rows of this run: hotels, price ladders by booking site, calendar nights or reviews, depending on the outputType you chose.

## `errors` (type: `string`):

Every error of the run with its reason and what to do. Always present, empty when nothing failed.

## `summary` (type: `string`):

Counts of delivered, charged, duplicate and error rows for this run.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "outputType": "hotel",
    "locations": [
        "Lisbon"
    ],
    "maxHotels": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("oswaldocarabano/google-hotels-prices-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "outputType": "hotel",
    "locations": ["Lisbon"],
    "maxHotels": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("oswaldocarabano/google-hotels-prices-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "outputType": "hotel",
  "locations": [
    "Lisbon"
  ],
  "maxHotels": 20
}' |
apify call oswaldocarabano/google-hotels-prices-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,oswaldocarabano/google-hotels-prices-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/YQpOjxo3gMpDCkGAF/builds/EX0kcpMihxNhhPgjf/openapi.json
