# Google Hotels Scraper - Hotel Prices Across Every Booking Site (`lanternlab318/google-hotels`) Actor

Search Google Hotels by destination and dates: hotel listings plus the price every booking site quoted for the same stay.

- **URL**: https://apify.com/lanternlab318/google-hotels.md
- **Developed by:** [Lantern Lab](https://apify.com/lanternlab318) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.99 / 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.

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

Give it a destination and your dates and get Google Hotels' public results back: the hotel, its class, rating, review count, coordinates, address, amenities, photos, the cheapest nightly price and stay total - and `prices`, the quote **every booking site** gave for that same stay, with Google's own outbound link for each. Your dates, guest count and currency are honoured, not approximated. No Google login, no cookies, no account of your own. **$0.99 per 1,000 hotels, no start fee; every booking site's price is included.** See [Pricing](#pricing).

Input:

```json
{
  "queries": ["hotels in Paris", "hotels in Tokyo"],
  "checkIn": "2026-11-10",
  "checkOut": "2026-11-12",
  "adults": 2,
  "currency": "USD",
  "maxItems": 40
}
```

Output - one row per hotel (`recordType: "hotel"`); per-destination status rows and the run summary are free. Sample from a real run, photo and link URLs shortened:

```json
{
  "recordType": "hotel",
  "hotelId": "ChgI9LOche-i1LjzARoLL2cvMXRkM2t4YjMQAQ",
  "featureId": "0x47e66d84b6c685f7:0xf3715116f0a719f4",
  "name": "Hôtel Belgrand",
  "url": "https://www.google.com/travel/hotels/entity/ChgI9LOche-i1LjzARoLL2cvMXRkM2t4YjMQAQ",
  "starClass": 3,
  "starClassText": "3-star tourist hotel",
  "rating": 3.4,
  "reviewCount": 440,
  "address": "35 Rue Belgrand, 75020 Paris, France",
  "lat": 48.8644845,
  "lng": 2.4062453,
  "countryCode": "FR",
  "lowestPrice": 86.2708,
  "lowestPriceText": "$86",
  "totalPrice": 173,
  "totalPriceText": "$173",
  "currency": "USD",
  "prices": [
    { "provider": "Booking.com", "price": 86.27, "priceText": "$86", "totalPrice": 173, "totalPriceText": "$173", "link": "https://www.google.com/travel/lodging/clk?pc=AA80Osw6…" },
    { "provider": "Expedia.com", "price": 92.5, "priceText": "$93", "totalPrice": 185, "totalPriceText": "$185", "link": "https://www.google.com/travel/lodging/clk?pc=AA80OsyR…" }
  ],
  "pricesStatus": "ok",
  "amenities": ["Breakfast ($)", "Free Wi-Fi", "Parking", "Restaurant", "Accessible", "Kid-friendly", "Smoke-free property"],
  "imageUrls": ["https://lh3.googleusercontent.com/grass-cs/AABkmLcNHAM7…"],
  "description": "Unassuming rooms with flat-screens in a straightforward budget hotel offering complimentary Wi-Fi.",
  "website": "https://hotel-belgrand-paris.example-hotel-site.fr/",
  "checkIn": "2026-11-10",
  "checkOut": "2026-11-12",
  "nights": 2,
  "adults": 2,
  "pricedCheckIn": "2026-11-10",
  "pricedCheckOut": "2026-11-12",
  "pricedAdults": 2,
  "query": "hotels in Paris",
  "destination": "Paris",
  "destinationId": "0x47e66e1f06e2b70f:0x40b82c3688c9460",
  "hl": "en",
  "gl": "us",
  "rank": 1,
  "partial": false,
  "partialReason": null,
  "publishedAt": null,
  "collectedAt": "2026-10-24T10:41:22.318Z"
}
```

Limits:

- **About 20 hotels per destination.** That is one Google Hotels results page, and Google loads the rest only through an in-page call that needs a signed browser session. Use several narrower destinations ("hotels in Montmartre, Paris", "hotels near Gare du Nord") to cover a big city. Every destination row says how many Google listed (`hotelsAvailable`) and whether its page was full (`pageLimitReached`).
- **Prices move, and `lowestPrice` is not the cheapest row in `prices`.** `lowestPrice` / `totalPrice` are what Google printed on the results page - its featured rate for that hotel. `prices[]` comes from the hotel's own page a moment later and regularly contains a cheaper quote, which is the point of fetching it. Both are kept as read; neither is recomputed from the other. A row is a quote at `collectedAt` for the stay in `checkIn`/`checkOut`, not a reservation - re-run for current prices, and expect different prices from a different `gl`.
- **How many booking sites appear is Google's choice, not a completeness guarantee.** Measured 2026-10-24: 13-31 offers per hotel for Paris, Tokyo, Bangkok, Dubai and Singapore, exactly 1 (the hotel's own site) for Moscow hotels, and occasionally a short list where a repeat of the same request returned the full one. `prices[]` is what the hotel page carried when it was read; `pricesStatus` tells you whether it could be read at all.
- **Public data only.** The public Google Hotels result page and hotel page; no login, no reviews text, no room-level inventory, no guest names or contact details.
- **Every destination gets a status row saying what happened.** A destination Google answered with nothing is an empty result; one cut short by a limit, refusal or error is `partial` with the reason - the two are never confused.
- **Your limits stop collection and appear in the row**: `maxItems`, the `maxUpstreamRequests` request ceiling and the maximum charge per run (see [Pricing](#pricing)).

### What it does

- Searches each destination separately and labels every row with the destination that produced it, so one run can cover many cities or districts.
- Sends your stay exactly as Google's own date picker does, so the prices are for **your** dates, your guest count and your currency - not for Google's defaults. Google ignores the `checkin=`/`checkout=`/`curr=` parameters people usually try; this Actor does not use them.
- Checks Google's answer against what was asked. Every row carries `pricedCheckIn` / `pricedCheckOut` / `pricedAdults` - the stay Google says it priced. If it ever differs from yours, the rows are marked `partial` with `partialReason: "stay_not_applied"` **and not charged**, instead of being handed to you as prices for the wrong dates.
- Fetches each hotel's own page for the street address and the full `prices` list - one row per booking site, as Google ordered them, each with its nightly price, its stay total and Google's outbound link.
- Deduplicates by `hotelId` across the whole run, so a hotel that matches two of your destinations is written and charged once.
- Says which place Google decided your search term meant, in `destination`. Google answers a term it cannot place with hotels somewhere else entirely rather than with nothing, and this is the field that shows it.
- Writes `RUN_SUMMARY` as the last dataset row and as a key-value store record: totals, empty destinations, incomplete destinations by reason, provider offers read, requests sent and why collection stopped.

### Input

| Field | Type | Default | What it does |
| --- | --- | --- | --- |
| `queries` | string\[] | required | One destination per line: a city, a district, a region, or a hotel name. |
| `checkIn` | string | tomorrow | Check-in day, `YYYY-MM-DD`. Must be today or later, at most 330 days ahead. |
| `checkOut` | string | day after check-in | Check-out day, `YYYY-MM-DD`. After check-in, at most 30 nights later. |
| `adults` | integer | `2` | Guests the rooms are priced for, 1-6. |
| `currency` | string | `USD` | Three-letter code the prices come back in, e.g. `EUR`, `GBP`, `RUB`, `JPY`. |
| `gl` | string | `us` | Two-letter country code of the Google edition; changes which booking sites are listed. |
| `hl` | string | `en` | Interface language; the language of amenity names, the class sentence and the description. |
| `maxItems` | integer | `100` | Upper bound on hotel rows for the whole run. |
| `includeProviderPrices` | boolean | `true` | Off: no `prices` list, no `address`, one request per destination instead of one per hotel. |
| `maxUpstreamRequests` | integer | derived | Hard ceiling on requests to Google. Left empty it is worked out from your input, so a run is never unbounded. |
| `proxyConfiguration` | object | Apify Proxy | Where the requests come from. The default datacenter proxy is what this Actor is measured on. |

The 330-day and 30-night bounds are Google's own, taken from its date picker; an input outside them fails before any request is sent, so it costs nothing.

### Output

Three record types in one dataset, told apart by `recordType`:

**`hotel`** - one per hotel, the only charged row. `prices[]` holds `{ provider, price, priceText, totalPrice, totalPriceText, link }` per booking site. A field Google did not provide is `null`, never guessed: `starClass` is absent for apartments and some vacation rentals, `rating` and `reviewCount` for places with no reviews yet, `address` on runs with `includeProviderPrices` off. `price`/`totalPrice` are numbers; `priceText`/`totalPriceText` are what Google displayed, kept verbatim.

`pricesStatus` says why `prices` is empty:

| `pricesStatus` | Meaning |
| --- | --- |
| `ok` | Offers were read. |
| `no_offers` | Google showed no bookable offer for this hotel and stay. A real answer. |
| `not_requested` | `includeProviderPrices` was off. |
| `upstream_request_cap` | The run's request ceiling was reached before this hotel's page. |
| `blocked` | Google refused the hotel page. |
| `request_failed` / `unreadable` | The hotel page failed, or carried no readable payload. |

**`query_status`** - one per destination, never charged. `hotelsReturned`, `hotelsCharged`, `duplicatesSkipped`, `hotelsAvailable`, `pageLimitReached`, `partial`, `partialReason`, `statusText`, and `destination` (the place Google resolved the term to).

| `partialReason` | Meaning |
| --- | --- |
| `null` | Complete. `hotelsReturned: 0` with `partial: false` means Google really listed no hotel. |
| `page_limit_reached` | You had room for more rows and Google's one page ran out first. |
| `max_items_reached` | `maxItems` was reached here. |
| `upstream_request_cap` | The request ceiling was reached here. |
| `charge_limit_reached` | The run's maximum charge was reached here. |
| `blocked` | Google refused our requests for this destination. Not an empty result. |
| `upstream_error` | A request failed in a way a retry inside the run would not fix. |
| `unreadable_page` | Google answered with a page holding no readable hotel list. |
| `stay_not_applied` | Google priced a stay other than yours. The rows are kept, marked, and not charged. |
| `not_collected` | The run stopped earlier and never reached this destination. |

**`RUN_SUMMARY`** - the last row, and the `RUN_SUMMARY` key-value store record.

### Pricing

**$0.99 per 1,000 hotel rows. No start fee, no per-page fee, no charge for the booking-site prices.**

- Only `hotel` rows are charged. Destination status rows and `RUN_SUMMARY` are free, so a destination that returned nothing, was blocked or was cut short costs nothing.
- Rows marked `partial` are not charged. In practice that means a stay Google did not apply: you can see what came back without paying for prices that are not for your dates.
- A hotel matching several of your destinations is charged once.
- Set **Maximum cost per run** in the run options to cap a run. Collection stops at the cap and the destination it stopped on is marked `charge_limit_reached` - it is never reported as empty.

### Notes

- `includeProviderPrices` is the one setting that changes the cost of a run for us as well as the time: it is one extra request and roughly 25 times more traffic per hotel. The price above covers it either way; turning it off just makes runs faster.
- Measured on Apify Proxy datacenter IPs. Google Hotels serves these public pages over them without a bot check, so no residential proxy is needed; you can still point `proxyConfiguration` at one if you want a specific country's exit.
- `hotelId` is Google Travel's own identifier and is stable across runs; `featureId` is the Google Maps feature id for the same place, for joining to other Google data.

# Actor input Schema

## `queries` (type: `array`):

One destination per line: a city ("Paris"), a district ("Shibuya, Tokyo"), a region, or the name of one hotel. Every entry is searched separately and its rows are labelled with it. A hotel that matches several entries is returned once and charged once. Google serves one page of about 20 hotels per destination, so use several narrower entries when you need more of a big city.

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

Check-in day as YYYY-MM-DD, for example 2026-11-10. Leave empty for tomorrow. Google Hotels only prices future stays, at most 330 days ahead.

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

Check-out day as YYYY-MM-DD. Leave empty for the day after check-in. Must be after check-in, at most 30 nights later - that is Google's own limit.

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

Number of adults the rooms are priced for, 1 to 6. The prices Google shows depend on it.

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

Three-letter code the prices are returned in, for example USD, EUR, GBP, RUB, JPY. Google converts the booking sites' own quotes into it. Left alone, prices come back in USD regardless of where the run exits to the internet.

## `gl` (type: `string`):

Two-letter country code of the Google Travel edition to search, for example "us", "de", "ru". It changes which booking sites Google lists and sometimes the ranking.

## `hl` (type: `string`):

Interface language as a BCP-47 tag: "en", "ru", "de", "ja", "pt-BR". It is the language of the text fields - amenity names, the hotel class sentence and the description.

## `maxItems` (type: `integer`):

Upper bound on the hotel rows the whole run may produce. Google returns about 20 hotels per destination and offers no way to page past that without a browser session, so the row count is often Google's ceiling rather than this setting - the run says which, per destination.

## `includeProviderPrices` (type: `boolean`):

With this on, every hotel also gets prices\[]: each booking site Google listed for your dates, its nightly price, its stay total and Google's own outbound link, plus the hotel's street address. That needs one extra request per hotel and moves about 25 times more data than the hotel list alone, which is the main thing driving the cost of a run. Turn it off for a fast, cheap run that keeps the cheapest price Google showed and leaves prices\[] empty and address null.

## `maxUpstreamRequests` (type: `integer`):

Hard ceiling on the HTTP requests this run sends to Google: search pages, hotel pages and retries together. At the ceiling collection stops - the destinations not reached are written with partialReason "upstream_request_cap" and the run then ends normally. Leave empty and the Actor works a ceiling out from your destinations and limits, so a run is never unbounded.

## `proxyConfiguration` (type: `object`):

The proxy the requests to Google go through. The default (Apify Proxy, datacenter) is the cheapest and is what this Actor is measured on; Google Travel serves its public hotel pages over it without a bot check.

## Actor input object example

```json
{
  "queries": [
    "hotels in Paris"
  ],
  "adults": 2,
  "currency": "USD",
  "gl": "us",
  "hl": "en",
  "maxItems": 100,
  "includeProviderPrices": true,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

No description

## `runSummary` (type: `string`):

No description

# 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 = {
    "queries": [
        "hotels in Paris"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("lanternlab318/google-hotels").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 = { "queries": ["hotels in Paris"] }

# Run the Actor and wait for it to finish
run = client.actor("lanternlab318/google-hotels").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 '{
  "queries": [
    "hotels in Paris"
  ]
}' |
apify call lanternlab318/google-hotels --silent --output-dataset

```

## MCP server setup

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

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/r9gR6D3xDp27YtbvT/builds/BWwTKrJT7XB4oDjZ9/openapi.json
