DEV Community

elliott maqy
elliott maqy

Posted on

How to get Google Hotels prices with Python

Google Hotels is where many trips start: type "hotels in Lisbon", pick dates, and every hotel shows a nightly price and the list of booking sites that sell it, from Booking.com and Expedia to the hotel's own website. Google offers no public API to read those prices, and copying them by hand stops being fun after the third hotel. This tutorial gets Google Hotels prices with Python: the lowest price per night and for the whole stay, the rate of every booking site with its direct link, a CSV export, and a rate parity check between the hotel's website and the online travel agencies. The data comes from the Google Hotels Scraper, an Apify Actor called through the official Python client.

What you need

  • An Apify account (the free plan is enough to try this) and its API token, from Apify Console → Settings → API & Integrations, exported as an environment variable: export APIFY_TOKEN=...
  • Python 3 and two packages:
pip install "apify-client>=3,<4" pandas
Enter fullscreen mode Exit fullscreen mode

What one row contains

The Actor runs a Google Hotels search for one stay and returns one row per hotel:

  • prices in currency: pricePerNight (before taxes) and pricePerNightWithTaxes, priceTotal (the whole stay, taxes and fees included), priceTotalBeforeTaxes and taxesAndFees;
  • the hotel: title, propertyType, starRating, rating on 5 from Google reviews, reviewCount, amenities, latitude and longitude, badges (Google's deal note, such as "20% less than usual");
  • the stay: checkIn, checkOut, nights, guests;
  • with includeOffers, the booking sites: offers (one entry per site, cheapest first), offerCount, cheapestOfferSite, cheapestOfferPriceTotal and officialSitePriceTotal, the price on the hotel's own website.

platformListingId is Google's id of the hotel, a stable key to join several runs.

Step 1: the input

This is the public task "Compare booking sites for Lisbon hotels next month":

run_input = {
    "searchQueries": ["hotels in Lisbon"],
    "checkInDaysFromNow": 30,
    "nights": 3,
    "adults": 2,
    "currency": "USD",
    "maxResults": 50,
    "includeOffers": True,
}
Enter fullscreen mode Exit fullscreen mode

The stay is relative: check-in 30 days after today, 3 nights, so the same input still works next month. For a precise stay, replace the two relative fields with "checkIn": "2026-12-04" and "checkOut": "2026-12-07". searchQueries is written as on Google ("hotels in Porto", "hotels near Times Square"); each line is its own search with the same stay, and maxResults caps each one. country (default us) picks the Google market, language the labels.

includeOffers adds one request per hotel (about a second each, three at a time), so the run takes a little longer. The booking sites cost nothing extra.

Step 2: run it and save a CSV

Save this as google_hotels.py:

import os

import pandas as pd
from apify_client import ApifyClient

run_input = {
    "searchQueries": ["hotels in Lisbon"],
    "checkInDaysFromNow": 30,
    "nights": 3,
    "adults": 2,
    "currency": "USD",
    "maxResults": 50,
    "includeOffers": True,
}

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("myth84/google-hotels-scraper").call(run_input=run_input)
rows = list(client.dataset(run.default_dataset_id).iterate_items())

for item in rows[:5]:
    print(item["title"], item["pricePerNightWithTaxes"], item["priceTotal"], item["currency"], item["cheapestOfferSite"])

hotels = pd.DataFrame(rows)
columns = [
    "title", "starRating", "rating", "reviewCount", "checkIn", "nights", "currency",
    "pricePerNight", "pricePerNightWithTaxes", "priceTotal", "offerCount",
    "cheapestOfferSite", "cheapestOfferPriceTotal", "officialSitePriceTotal", "url",
]
hotels[columns].sort_values("priceTotal").to_csv("lisbon-hotels.csv", index=False)
print(f"{len(hotels)} hotels saved to lisbon-hotels.csv")
Enter fullscreen mode Exit fullscreen mode

priceTotal is the column to compare: the lowest price of the stay with taxes and fees, whatever the site. A hotel with no price for your dates stays in the file with empty prices (isAvailable is false), at the bottom of the sort.

Step 3: every booking site in one table

The offers column is a list with one entry per booking site: site, isOfficialSite, pricePerNightWithTaxes, priceTotalWithTaxes and bookingUrl, the direct link to that site's page for the stay. pandas flattens it into one row per hotel and site; add this to the script:

offers = pd.json_normalize(rows, record_path="offers", meta=["platformListingId", "title"])
offers.to_csv("lisbon-booking-sites.csv", index=False)

grid = offers.pivot_table(index="title", columns="site", values="priceTotalWithTaxes", aggfunc="min")
lowest = grid.min(axis=1)
premium = grid.sub(lowest, axis=0).div(lowest, axis=0).mul(100)

sites = pd.DataFrame({"hotels": grid.notna().sum(), "medianPremiumPercent": premium.median().round(1)})
print(sites.query("hotels >= 10").sort_values("medianPremiumPercent"))
print(hotels["cheapestOfferSite"].value_counts().head(10))
Enter fullscreen mode Exit fullscreen mode

grid has one row per hotel and one column per booking site, with the price of the stay including taxes. premium is how much more each site asks than the cheapest one for the same hotel, in percent. The first table lists the sites that price at least ten hotels, from the most to the least competitive: a median of 0 means the site is usually the cheapest or tied with it. The second counts how often each site is the cheapest; a hotel's own website appears under the hotel's name.

lisbon-booking-sites.csv keeps every rate with its bookingUrl, ready for a spreadsheet or a price comparison page.

Step 4: the rate parity check

Rate parity is the rule many hotels try to keep: their own website should not be more expensive than the agencies. Two summary columns make it a few lines:

parity = hotels.dropna(subset=["officialSitePriceTotal", "cheapestOfferPriceTotal"]).copy()
parity["gap"] = parity["officialSitePriceTotal"] - parity["cheapestOfferPriceTotal"]
parity["gapPercent"] = (100 * parity["gap"] / parity["cheapestOfferPriceTotal"]).round(1)

undercut = parity[parity["gap"] > 0].sort_values("gapPercent", ascending=False)
print(f"{len(undercut)} of {len(parity)} hotels are cheaper elsewhere than on their own website")
print(undercut[["title", "cheapestOfferSite", "cheapestOfferPriceTotal", "officialSitePriceTotal", "gapPercent"]].head(15))
Enter fullscreen mode Exit fullscreen mode

cheapestOfferPriceTotal is the lowest price among all the sites, the official one included, so the gap is never negative: 0 means the hotel's website is the cheapest or tied. For a hotel, the list shows which agency undercuts it and by how much; for a traveller, it says when booking direct costs more. Run it on 4- and 5-star hotels only with parity[parity["starRating"] >= 4], as in the public task "Rate parity check for 4- and 5-star hotels in Rome".

Without code, and every day

Open the public task in Apify Console, change the city and the stay, run it, and download the Offers view of the dataset as CSV or Excel. Save it as your own task and add a schedule to get a fresh file every morning.

To follow price changes, add "monitorPrices": True: each hotel's priceTotal is compared with your previous run of the same search, and the rows carry changeType (new, price_changed or unchanged), previousPrice, priceDelta and priceDeltaPercent. With "onlyChanges": True, only the new hotels and the price changes are saved and charged. The public task "Track hotel prices in London every day" is a ready example.

Good to know

  • Google's order varies from one request to the next: the Actor removes the hotels seen twice and stops at maxResults. Google's own sort and filters are not applied; filter the DataFrame on starRating, rating or priceTotal instead.
  • Prices are those of a visitor without an account in the chosen market and currency. A site may show another price after the click (member prices, other room types).
  • One rate per booking site for the stay, not every room type. The booking-site rates are verified for 2 adults; other occupancies are passed to Google as they are.
  • USD and EUR are validated; other currencies Google lists are accepted.
  • A run that finds no hotel fails with an explicit message (search not recognised, dates in the past), so a broken schedule never looks like an empty market.
  • No personal data: hotel facts, the average rating, the breakdown by stars and the number of reviews, never review texts or authors.

Pricing is pay-per-event: $2.50 per 1,000 hotels on Apify's Free plan, down to $1.69 on the higher plans, every booking site's rate included (this run of 50 hotels costs about $0.13). Use the data for analysis, respect the platform's terms and local law, and do not republish listings.

Disclosure: I build these Actors and may earn a commission if you sign up to Apify through these links.

Top comments (0)