If you run a hotel, a travel site, or just a spreadsheet of competitors you want to keep an eye on, the question that comes up every morning is simple: are my competitors cheaper than me today, and by how much?
Google Hotels is the easiest place to answer it. For every hotel it shows the lowest price from 20-40 booking sites plus the hotel's own website, for the exact dates you pick, in any currency. There is no official API, but you can get the same data with a one-page Python script and a daily schedule.
This post shows the full setup. Code is in github.com/huynhtanan/google-hotels-api (MIT). Disclosure: I built the Apify scraper this uses; it's paid per result, new Apify accounts get free monthly credit that covers a few thousand hotels.
The script: one SQLite table, one function per concern
Give it a list of hotel names (or hotel URLs from Google Hotels) and a check-in date. Each run saves one row per hotel to a local SQLite file and prints how prices moved versus the previous observation.
# track_prices.py — see github.com/huynhtanan/google-hotels-api/blob/main/python/track_prices.py
import os, sqlite3
from datetime import date, timedelta
from pathlib import Path
from apify_client import ApifyClient
HOTELS = ["Hilton Paris Opera", "Pullman Paris Tour Eiffel", "Le Meurice Paris"]
CHECK_IN = date.today() + timedelta(days=30)
DB = Path("prices.sqlite3")
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("zet46/google-hotels-scraper").call(run_input={
"queries": HOTELS, "maxHotelsPerQuery": 1,
"checkIn": CHECK_IN.isoformat(),
"checkOut": (CHECK_IN + timedelta(days=1)).isoformat(),
"currency": "EUR",
})
items = list(client.dataset(run.default_dataset_id).iterate_items())
with sqlite3.connect(DB) as con:
con.execute("""CREATE TABLE IF NOT EXISTS prices (
run_at TEXT, hotel TEXT, check_in TEXT, nightly REAL,
PRIMARY KEY (run_at, hotel, check_in))""")
for h in items:
con.execute("INSERT OR REPLACE INTO prices VALUES (?,?,?,?)",
(str(run.started_at), h["name"], CHECK_IN.isoformat(), h.get("pricePerNight")))
Full version (with the "DOWN 5.4% (was 280.00 on 2026-10-15)" comparison printing) is in the repo.
First run, then the next day
$ python track_prices.py
...Status: SUCCEEDED, Message: Done: 3 hotels from 3 queries
Prices on 2026-10-15 for check-in 2026-11-14 (EUR):
Hilton Paris Opera 280.00 (first observation)
Le Meurice 1535.00 (first observation)
Pullman Paris Tour Eiffel 320.00 (first observation)
The next time the script runs it compares against the previous row for the same (hotel, check_in) and prints the direction and the diff:
Hilton Paris Opera 265.00 DOWN 5.4% (was 280.00 on 2026-10-15)
Le Meurice 1535.00 flat 0.0% (was 1535.00 on 2026-10-15)
Pullman Paris Tour Eiffel 340.00 UP 6.3% (was 320.00 on 2026-10-15)
Because the data lives in SQLite, you can plot the whole history or alert yourself with one SQL line:
-- competitors who undercut their own 7-day average by 10% or more
SELECT hotel, nightly, avg7
FROM (
SELECT hotel, nightly, run_at,
AVG(nightly) OVER (PARTITION BY hotel, check_in
ORDER BY run_at ROWS BETWEEN 7 PRECEDING AND 1 PRECEDING) AS avg7
FROM prices)
WHERE avg7 IS NOT NULL AND nightly < avg7 * 0.9
ORDER BY run_at DESC;
Running it every day
Two options, both free:
-
Apify Scheduler (zero infrastructure). On the actor page click Schedule actor, pick a cron (e.g. daily 09:00), save your input (hotel list + dates). Then from any machine run the script above with
client.actor(...).last_run().get()to pull the latest completed dataset. No local cron, no server. -
Local cron / Windows Task Scheduler. Call
python track_prices.pydaily. Each run also triggers a fresh scrape, so this is enough on its own.
Cost
The scraper charges $2 per 1,000 hotels. Ten competitors tracked daily is ~300 hotels/month, so about $0.60/month. Turn on includeProviderPrices: true only if you also want the per-site breakdown (Booking.com vs Expedia vs the hotel's website); it doubles the per-hotel cost but lets you catch rate-parity breaks on specific channels.
A couple of things that bit me
-
pricePerNightis the nightly price including taxes that Google displays.totalPriceis the whole stay including taxes and fees. If you compare across sources, pick one and stick with it. - Dates and currency are not URL query parameters on Google Hotels; they are encoded in the
tsprotobuf blob. The scraper handles that for you, which is why calling it directly is a lot less work than parsing Google's HTML yourself. - Hotel names are matched by Google's own search, so "Hotel Lutetia Paris" may silently return 0 results if Google's suggest doesn't lock on; pass the hotel's Google Hotels URL (
/travel/hotels/entity/<token>) viahotelUrlswhen you need to be exact.
Feedback welcome
If you'd like a specific feature (price by room type, SQLite -> Google Sheets sync, a Discord alert webhook), the Apify actor repo issues are open and I ship fixes within a day. Code and more examples: github.com/huynhtanan/google-hotels-api.
Top comments (0)