kestrel actors docs

kestrel actors › Hotel & flight prices

Google Hotels Prices Scraper — Price Tracking & Rate Parity

Google Hotels prices scraper for exact dates and guests: the lowest nightly rate and the stay total for every hotel in a search, then every booking site's rate for each hotel — Booking.com, Expedia, Agoda, Hotels.com, Trip.com and the hotel's official site — with free-cancellation flags and deep links. Run it once for a price check, on a schedule for hotel price tracking and rate parity, or with a date sweep to build a price calendar and your own hotel price history. No Google API key, no login, no browser: it reads the same public Google Hotels results any visitor sees, through Apify's proxies, and returns clean JSON.

Run Google Hotels Prices Scraper on Apify →

Google hotels prices scraper pricing: what one row costs

EventWhat it isPriceCharged for
hotelHotel price row$0.004One hotel with its lowest nightly rate and stay total for your dates. Unpriced (sold-out) hotels are free.
offerSource rate row$0.002One rate from one source (OTA or official site) for one room and stay, with free-cancellation flag and deep link.

Pay per event, only for priced rows: a hotel row (a hotel with a price for your dates) and an offer row (one source's rate). Sold-out hotels, empty searches and status rows are free, and a hotel and stay is billed once per run even when overlapping searches return it twice. A city search of 60 hotels with source-level offers is a few hundred rows; a daily parity check of ten properties with seven check-in days is about 70 hotel rows plus their offers. Apify's platform usage for a run of that size is a fraction of a cent — the runs above cost $0.001–$0.003 in compute. The exact per-row prices are on this page's pricing tab, and maxOffersPerHotel caps the offer rows when you only need the cheapest few sources.

Google hotels prices scraper input reference

Every field of Google Hotels Prices — input, straight from the actor's input schema.

FieldTypeDefaultWhat it does
queriesarrayFree-text place searches, exactly as you would type them into Google Hotels: "hotels in Lisbon", "hotels near Times Square", "5-star hotels Bangkok". Google's results are paged 20 at a time; maxHotels sets how many distinct hotels to collect per place. Google always answers a place search: an unrecognised or misspelt place is silently matched to somewhere else rather than failing, and those hotels are delivered and billed. Pass an exact place name or a Google Hotels entity URL, and check the status row's spread_km (and each hotel's lat/lng) before trusting the results.
hotelsarray[]Track exact properties without a search: Google Hotels URLs (https://www.google.com/travel/hotels/entity/ChkI…) or bare entity tokens, e.g. from an earlier run's google_url/entity_id. One offers call per hotel and stay; the hotel row comes from that page.
checkInstringrequiredCheck-in date: absolute YYYY-MM-DD or relative to the run date (e.g. 2026-10-03, or relative for schedules: "40 days", "6 weeks", "+40d"). Relative dates keep a scheduled run valid forever.
checkOutstringrequiredCheck-out date: absolute YYYY-MM-DD or relative to the run date (e.g. 2026-10-06 or "42 days" — must be after check-in; nights = the difference). Relative dates keep a scheduled run valid forever.
sweepDaysinteger00 = only the stay above. N = also fetch the same length of stay starting on each of the next N days (a 3-night stay with sweepDays 6 = 7 stays, one per check-in day). Each stay is a separate list call. Range 0-60.
adultsinteger2Adults per room (prices scale with occupancy exactly as on Google). Range 1-6.
childrenAgesarray[]Ages of children sharing the room, e.g. [4, 9]. Leave empty for none.
currencystring"USD"3-letter currency code for every price (USD, EUR, GBP, JPY, AED, …). All rows are in this one currency.
languagestring"en"Google UI language (hl): affects labels such as room names and deal notes.
countrystring"us"Google market (gl): which country's Google Hotels answers. Prices and available sources differ by market.
maxHotelsinteger20Distinct hotels to collect per place and stay. Google returns 20 per page; the actor pages on (one list call per 20) until this many or the results end. Range 1-200.
includeOffersbooleantrueFor each priced hotel also fetch its offers page (one extra call per hotel): the rates from every source — Booking.com, Expedia, Agoda, the official site… — with free-cancellation flag, stay total and deep link.
offerLevelstring (enum)"sources"sources = one row per source with its lowest rate (rate parity; ~5–15 rows per hotel). rooms = every source × room × rate, with room names and cancellation deadlines (~50–100 rows per hotel). One of: sources, rooms.
maxOffersPerHotelinteger00 = every source. N = keep only the N cheapest rows per hotel (cost control; the official site row is kept only if it is among them). Range 0-500.
proxyConfigurationobject{"useApifyProxy": true}Google accepts Apify datacenter proxies for these calls; keep the default. Residential is unnecessary.
sessionsinteger4Concurrent egress IPs; each is paced at perIp requests per second. Range 1-16.
perIpnumber0.5Pace per proxy session. 0.5 is safe; raise carefully. Range 0.1-2.

Example input

{ "queries": ["hotels in Lisbon"], "checkIn": "2026-10-03", "checkOut": "2026-10-06", "adults": 2, "currency": "USD", "maxHotels": 60 }
{ "hotels": ["https://www.google.com/travel/hotels/entity/ChkIhLCQwvjO2IYWGg0vZy8xMXE0bTZieDkyEAE"], "checkIn": "30 days", "checkOut": "32 days", "sweepDays": 6, "currency": "EUR", "offerLevel": "sources" }

Google hotels prices scraper output fields

Three row types share one dataset: hotel (lowest nightly rate and stay total per hotel), offer (one booking source's rate for a hotel and stay), status (per place or property and stay; never charged).

Dataset views in the Apify Console: Overview, Hotels, Offers by source, Run status.

FieldTypeWhat it holds
typestringRow type: hotel, offer or status.
querystringThe place search this row came from (null in property mode).
hotelstringStatus rows in property mode: the hotel URL or entity token that was priced.
check_instringCheck-in date, YYYY-MM-DD.
check_outstringCheck-out date, YYYY-MM-DD.
nightsintegerNights in the stay.
adultsintegerAdults per room used for pricing.
childrenintegerChildren per room used for pricing.
currencystringCurrency of every price in the row (ISO code).
rankintegerPosition in Google's results for the place search (null in property mode).
namestringHotel name.
hotel_idstringGoogle's numeric hotel id.
entity_idstringGoogle Hotels entity token; reuse it in the hotels input to price this property directly.
cidstringGoogle Maps place id (CID).
starsintegerHotel class, 1–5.
class_labelstringGoogle's class label, e.g. 4-star hotel.
ratingnumberGoogle rating, 1–5.
reviewsintegerNumber of Google reviews.
latnumber
lngnumber
descriptionstringGoogle's one-line description of the hotel.
websitestringHotel website.
typesarrayGoogle place types, e.g. hostel, boutique_hotel.
checkin_timestring
checkout_timestring
thumbnailstringThumbnail image URL.
nightlynumberLowest nightly rate (hotel rows) or this source's nightly rate (offer rows). Null = no price for the stay (sold out).
nightly_displaystringNightly rate as Google displays it, e.g. $82.
totalnumberTotal for the whole stay.
total_displaystringStay total as Google displays it.
dealstringGoogle's deal note, e.g. 19% less than usual.
sourcestringBooking source: an OTA name, or the hotel's own name when official is true. On hotel rows: the source of the lowest rate when Google names it.
source_idintegerGoogle's id for the source — the key of a source (a name can appear twice: official listing and marketplace rate).
officialbooleanTrue when the source is the hotel's official site.
roomstringRoom name (offerLevel rooms only).
max_guestsintegerGuests the rate covers (offerLevel rooms only).
free_cancelbooleanFree cancellation available.
free_cancel_untilstringFree-cancellation deadline as Google shows it, e.g. Oct 1.
linkstringGoogle's click-through link for the source.
partner_urlstringThe source's own URL with the dates filled in (official sites: the booking engine).
lowestbooleanTrue for a source's lowest rate row.
maps_urlstringGoogle Maps link for the hotel.
google_urlstringGoogle Hotels page of the hotel.
total_resultsintegerGoogle's result count for the place search (capped at 15000).
hotelsintegerStatus rows: hotel rows produced.
pricedintegerStatus rows: hotels with a price (the only ones charged).
offersintegerStatus rows: offer rows produced.
duplicatesintegerStatus rows: hotels skipped because another search already delivered them for this stay.
offer_errorsintegerStatus rows: hotels whose offers page failed.
errorstringStatus rows: error message, null when the job succeeded.
fetched_atstringUTC timestamp of the fetch.
spread_kmnumberStatus rows, place mode only: median distance in km of the returned hotels from their centre. A city is single digits, a region tens. A large value means Google may not have resolved your place and answered with scattered unrelated hotels — those rows are still billed, so check this before trusting a run.

Example row from a real run

Taken from the actor's README — this is the shape your dataset holds.

{ "type": "hotel", "query": "hotels in Lisbon", "check_in": "2026-10-03", "check_out": "2026-10-06", "nights": 3, "adults": 2, "children": 0, "currency": "USD",
  "rank": 1, "name": "The Central House Lisbon Baixa", "hotel_id": "10088532857338925820", "entity_id": "ChkIg-b2ismUj7M1Gg0vZy8xMWg3MThreGg1EAE",
  "stars": 2, "class_label": "2-star hotel", "rating": 4.3, "reviews": 727, "lat": 38.7131, "lng": -9.1368, "types": ["hostel"],
  "nightly": 81.81, "nightly_display": "$82", "total": 245, "total_display": "$245", "deal": "19% less than usual", "source": null,
  "website": "https://www.thecentralhousegroup.com/", "maps_url": "https://maps.google.com/?cid=3848859359889388291", "thumbnail": "https://lh3.googleusercontent.com/gps-cs-s/…",
  "google_url": "https://www.google.com/travel/hotels/entity/ChkIg-b2ismUj7M1Gg0vZy8xMWg3MThreGg1EAE", "total_results": 15000, "fetched_at": "2026-08-25T14:03:11+00:00" }
{ "type": "offer", "name": "Hyatt Regency Lisbon", "entity_id": "ChoI4cjBy-KGurGyARoNL2cvMTFtdnFfdncydhAB", "check_in": "2026-10-03", "check_out": "2026-10-06", "nights": 3, "adults": 2, "children": 0, "currency": "USD",
  "source": "Booking.com", "source_id": 184, "official": false, "room": null, "nightly": 569.35, "total": 1708.05, "max_guests": null,
  "free_cancel": null, "free_cancel_until": null, "link": "https://www.google.com/travel/lodging/clk?pc=…", "partner_url": "https://www.booking.com/hotel/pt/hyatt-regency-lisbon.html?checkin=2026-10-03&checkout=2026-10-06…", "lowest": true, "fetched_at": "2026-08-25T14:03:14+00:00" }

Google hotels prices scraper in Python, JavaScript and curl

Python

from apify_client import ApifyClient
client = ApifyClient("<TOKEN>")
run = client.actor("kestrel/google-hotels-prices").call(run_input={
    "queries": ["hotels in Lisbon"], "checkIn": "2026-10-03", "checkOut": "2026-10-06", "adults": 2, "currency": "USD", "maxHotels": 40})
rows = list(client.dataset(run["defaultDatasetId"]).iterate_items())
hotels = [r for r in rows if r["type"] == "hotel"]; offers = [r for r in rows if r["type"] == "offer"]
cheapest = min((h for h in hotels if h["nightly"]), key=lambda h: h["nightly"])
print(cheapest["name"], cheapest["nightly_display"], cheapest["total_display"])

JavaScript (Node)

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: '<TOKEN>' });
const run = await client.actor('kestrel/google-hotels-prices').call({ hotels: ['https://www.google.com/travel/hotels/entity/ChkIhLCQwvjO2IYWGg0vZy8xMXE0bTZieDkyEAE'], checkIn: '30 days', checkOut: '32 days', currency: 'EUR' });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
const official = Math.min(...items.filter(x => x.type === 'offer' && x.official).map(x => x.nightly));
const parityBreaches = items.filter(o => o.type === 'offer' && !o.official && o.nightly < official);
console.log(parityBreaches.map(o => [o.name, o.source, o.nightly]));

curl

curl -X POST "https://api.apify.com/v2/acts/kestrel~google-hotels-prices/run-sync-get-dataset-items?token=<TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"queries": ["hotels in Lisbon"], "checkIn": "2026-10-03", "checkOut": "2026-10-06", "maxHotels": 20, "includeOffers": false}'

The same three calls are runnable files in the examples repository: examples/google-hotels-prices.py|.js|.sh.

Limits and honest notes

  • A place search pages through Google's ranking 20 at a time up to maxHotels (200); neighbourhood or landmark queries add coverage beyond that.
  • Prices are what Google Hotels shows for the market (country) and currency you choose; whether taxes and fees are included follows Google's display rules for that market. The total is the stay total Google shows.
  • Sources per hotel vary by market and day (typically 5–25 with US settings). Sold-out hotels appear as hotel rows without a price and are not charged.
  • Google changes its internal formats occasionally; a daily canary run watches for that, the actor is rebuilt within days, and every run reports status rows so a failure is visible rather than a silent zero.
  • Google never fails a place search. An unrecognised or misspelt place is silently matched to somewhere else and answered with real hotels, which are delivered and billed like any other row. There is no signal in Google's response that says "I did not find that place", and a fuzzy-matched nonsense string can return a cluster as tight as a genuine region, so this cannot be detected reliably enough to refuse the run without also breaking legitimate broad searches like "Portugal" or "California". Each place status row therefore carries spread_km — the median distance of the returned hotels from their centre. A city is single digits, a region tens, a country a few hundred; anything in the thousands almost certainly did not resolve. Above 500km the run logs a warning. Pass an exact place name or a Google Hotels entity URL when it matters, and check spread_km and each hotel's lat/lng before trusting a run.

FAQ

Does it need a Google Hotels API key or a Google account?

No. There is no public Google Hotels API for reading prices; the scraper uses the public results page through Apify's proxies. No login, cookies or API key.

Can I get hotel price history and price alerts from Google Hotels?

Google's own tracker keeps history and alerts inside Google. Schedule this scraper (daily, with relative dates) and every run appends dated rows to your dataset — that is your price history — and an Apify integration (n8n, Make, Zapier, webhook) turns a drop into an alert.

How is this different from Google's "Track hotel prices" feature?

Google's feature emails you about the listings it chooses to watch and does not let you pick sources or export data. This scraper returns every source's rate for the exact dates, occupancy and currency you set, as rows you own.

Which booking sites are covered?

Whatever Google Hotels lists for that property and market: Booking.com, Expedia, Hotels.com, Agoda, Trip.com, Priceline, Kayak, eDreams, Vio.com, the official site and dozens of regional OTAs. The source name and source_id come straight from Google.

Does it include taxes and fees?

It returns the nightly price and the stay total Google Hotels displays for your market. In some markets Google shows all-in prices, in others taxes are added later; set country to the market you sell in and compare like with like.

How many hotels can one run return?

Up to 200 per place search, or as many properties as you list in hotels, times the number of stays (sweepDays). A run stops cleanly when it reaches the spending limit you set on the run.

Why do prices differ from what I see in my browser?

Google prices depend on market, currency, occupancy, dates and sometimes the device. Match country, currency, adults, childrenAges and the dates, and compare the total for the stay rather than a headline nightly figure.

Can I use it as a hotel price scraper for Booking.com or Expedia specifically?

Yes for prices: each offer row names its source, so filtering source == "Booking.com" gives you Booking.com's rate per hotel and stay, and likewise for Expedia, Agoda or Hotels.com — without scraping each site separately. For reviews or room descriptions use a Booking.com scraper or Expedia scraper built for that site.

Is there a free tier?

Yes: Apify's free plan covers small runs, and this scraper never charges for a run that returns no priced rows.

Last verified working: 2026-08-29.

How to use Google Hotels Prices Scraper

Ready-made runs you can open and start

Each one is a saved input on the Apify Store — open it, press Start, and it runs with the fields already filled in. All 163 across the suite.

Ready-made runWhat it does
Hotel Price History by Date (Google Hotels)Price one Barcelona hotel set across 14 consecutive check-in dates in a single run and build real hotel price history from Google Hotels.
OTA Rate Comparison: Booking vs ExpediaCompare Booking.com, Expedia, Agoda and direct rates for the same New York hotels and night as flat rows from Google Hotels.
Hotel Price Data by Room Type and SourceGet every source, room and rate with room names and cancellation deadlines from Google Hotels: room-level hotel price data for five hotels.
Scrape Google Hotels Prices for a CityOne search, 100 priced Rome hotels with star rating, guest score, location and the total for a three-night stay. A whole-city price snapshot.
Hotel Price Monitoring for a Comp SetTrack named properties, not a search: two Google Hotels entity URLs priced for seven consecutive check-in dates, six cents a run on a daily schedule.
Google Hotels API Alternative, No SERP KeyA ten-hotel Amsterdam run with every source rate: what a Google Hotels API would return, without a SERP key or a per-search fee.
Scrape Hotel Prices for a Family RoomTwo adults plus children aged 4 and 9 on a four-night Orlando stay: Google prices by occupancy, so a family room quote needs the ages set.
Hotel Price Tracking: Weekend vs WeekdaySeven consecutive one-night check-ins across 25 Chicago hotels isolate the weekend premium: same hotels, same stay length, only the weekday changes.
Hotel Rates API for the German MarketMunich hotels priced on the German Google market in EUR with German labels: the sources and rates a booker in Germany actually sees.
Hotel Price Intelligence: 5-Star DubaiSearch Google Hotels the way a guest would - "5-star hotels in Dubai" - and get 30 priced luxury properties in AED for a three-night stay.
Scrape Hotel Prices Near a Landmark"Hotels near Times Square" returns the walkable set, not all of New York: 40 priced properties with coordinates for a one-night stay.
Hotel Price Scraper for 7-Night StaysLong-stay pricing from Google Hotels: 30 Miami Beach hotels quoted for a full week, where weekly rates and minimum-stay rules change the answer.
Hotel Price Comparison API for Group RatesSix adults in one room across 20 Madrid hotels, with every source rate: which OTA handles high occupancy and which quietly drops the property.
Web Scraping Hotel Prices in Tokyo (JPY)Tokyo hotels priced in JPY on the Japanese Google market with Japanese labels: 40 properties, three nights, one currency across every row.

Run Google Hotels Prices Scraper

The actor lives on the Apify Store, where the pricing tab carries the live per-row price and the input form has every field above: apify.com/kestrel/google-hotels-prices