kestrel actors › Hotel & flight prices
Hotel Rate Parity Checker — OTA Price Comparison
Hotel rate parity checker built on Google Hotels prices: for each property and stay you name, one row with every booking source Google lists side by side — Booking.com, Expedia, Hotels.com, Agoda, Trip.com, Vio.com, the property's official site — and the parity math already done: the cheapest and dearest source, the spread in percent, the direct rate, and which OTAs undercut it and by how much. It is an OTA price comparison and rate shopping tool for revenue managers, channel managers and owners, priced per row instead of per seat. Put it on a schedule and it becomes rate parity monitoring with a history you own. No API key, no login, no browser: it reads the same public comparison any traveller sees, through Apify's proxies, and returns clean JSON.
Run Hotel Rate Parity Checker on Apify →
Hotel rate parity pricing: what one row costs
| Event | What it is | Price | Charged for |
|---|---|---|---|
parity | Parity row | $0.01 | One property and stay with every booking source's rate side by side (OTAs and the official site), the cheapest and dearest source, the spread, and whether and by how much the official site is undercut. Properties with no rates, unresolved names, filtered rows and status rows are free. |
Pay per event: one parity event per delivered parity row, at the price on this page's pricing tab. A comp set of ten properties checked over seven check-in dates is 70 rows; the same run with minSpreadPct set delivers — and bills — only the breaches. Properties with no rates for the stay, names that do not resolve, a property listed twice, rows removed by the filter, rows the spending limit refused and status rows are all free, and a property and stay is billed once per run. Apify's platform usage for such a run is a fraction of a cent: name lookups take one search each, every property and stay takes one offers call.
Compared with the parent Google Hotels Prices Scraper, which bills one row per source, a parity row is one event for the whole comparison — usually cheaper per property, and already in the shape a rate shopper wants.
Hotel rate parity input reference
Every field of Hotel Rate Parity — input, straight from the actor's input schema.
| Field | Type | Default | What it does |
|---|---|---|---|
hotels | array | — | The properties to check: Google Hotels URLs (https://www.google.com/travel/hotels/entity/ChkI…) or bare entity tokens, one per line. Find a URL by opening the property on google.com/travel/hotels; the entity_id of any earlier row works too. One offers call per property and stay — no search, so nothing unrelated is ever billed. |
hotelNames | array | [] | Property names, e.g. "Hyatt Regency Lisbon", "Brown's Avenue Hotel Lisbon". Each name costs one place search; the listing is kept only when its name matches most of the words you typed (at least two of them), otherwise the status row says not_found and nothing is billed. Add the city to a generic name. |
checkIn | string | required | Check-in date: absolute YYYY-MM-DD, or relative to the run date so a schedule never goes stale: "30 days", "6 weeks", "+30d", today, tomorrow. Past dates are rejected before anything is fetched. |
nights | integer | 1 | Length of stay in nights (1–30). Check-out = check-in + nights. Range 1-30. |
sweepDays | integer | 0 | 0 = only the stay above. N = also check the same length of stay starting on each of the next N days (7 = a week of check-in dates per property, one parity row each). Range 0-60. |
adults | integer | 2 | Adults per room. Rates scale with occupancy exactly as on Google, so compare like with like. Range 1-6. |
childrenAges | array | [] | Ages of children sharing the room, e.g. [4, 9]. Leave empty for none. |
currency | string | "USD" | 3-letter currency code for every price (USD, EUR, GBP, JPY, AED, …). |
language | string | "en" | Google UI language (hl); affects source labels. |
country | string | "us" | Google market (gl): which country's Google Hotels answers. Sources, prices and whether taxes are included differ by market — use the market you sell in. |
minSpreadPct | number | 0 | 0 = deliver every property. N = deliver only properties whose spread between the cheapest and dearest source is at least N % — the violations-only feed. Properties below the threshold get a free status row (status filtered) and are never billed. Range 0-1000. |
proxyConfiguration | object | {"useApifyProxy": true} | Google accepts Apify datacenter proxies for these calls; keep the default. Residential is unnecessary. |
sessions | integer | 4 | Concurrent egress IPs; each is paced at perIp requests per second. Range 1-16. |
perIp | number | 0.5 | Pace per proxy session. 0.5 is safe; raise carefully. Range 0.1-2. |
Example input
{ "hotels": ["https://www.google.com/travel/hotels/entity/ChkIhLCQwvjO2IYWGg0vZy8xMXE0bTZieDkyEAE", "ChkIg-b2ismUj7M1Gg0vZy8xMWg3MThreGg1EAE"], "checkIn": "30 days", "nights": 1, "adults": 2, "currency": "USD" }
{ "hotelNames": ["Hyatt Regency Lisbon", "Altis Grand Hotel Lisbon", "Brown's Avenue Hotel Lisbon"], "checkIn": "14 days", "nights": 2, "sweepDays": 6, "minSpreadPct": 5, "currency": "EUR", "country": "pt" }
Hotel rate parity output fields
Two row types share one dataset: parity (one property and stay with every booking source side by side and the parity math; charged) and status (per property and stay; never charged).
Dataset views in the Apify Console: Rate parity, Undercut check, Run status.
| Field | Type | What it holds |
|---|---|---|
type | string | Row type: parity (charged) or status (free). |
query | string | The input this row came from: the Google Hotels URL, entity token or property name as given. |
hotel_name | string | Property name as Google Hotels shows it. |
entity_id | string | Google Hotels entity token; reuse it in the hotels input. |
hotel_id | string | Google's numeric hotel id (name lookups only). |
hotel_url | string | Google Hotels page of the property. |
stars | integer | Hotel class, 1–5 (name lookups only). |
rating | number | Google rating, 1–5 (name lookups only). |
reviews | integer | Number of Google reviews (name lookups only). |
check_in | string | Check-in date, YYYY-MM-DD. |
check_out | string | Check-out date, YYYY-MM-DD. |
nights | integer | Nights in the stay. |
adults | integer | Adults per room used for pricing. |
children | integer | Children per room used for pricing. |
currency | string | Currency of every price in the row (ISO code). |
sources | array | Every booking source Google lists for the stay, cheapest first: {source, source_id, official, price_nightly, price_total, free_cancellation, url}. url is the source's own booking link with the dates filled in when Google exposes it. |
n_sources | integer | How many sources carried a rate. |
min_source | string | The cheapest source. |
min_price | number | Nightly rate of the cheapest source. |
max_source | string | The dearest source. |
max_price | number | Nightly rate of the dearest source. |
spread_pct | number | (max − min) ÷ min × 100, one decimal. The size of the parity gap across all sources. |
median_price | number | Median nightly rate across sources. |
official_source | string | The property's own listing (official site), when Google lists one. |
official_price | number | Nightly rate on the official site; null when Google lists no official site. |
official_is_cheapest | boolean | True when no source beats the official site; null when there is no official site. |
undercut_by | array | Sources cheaper than the official site, cheapest first. Empty when direct is the cheapest or there is no official site. |
undercut_pct | number | How far the cheapest source beats the official site: (official − min) ÷ official × 100. 0 when direct is cheapest; null when there is no official site. |
status | string | Status rows: ok, filtered, no_rates, not_found, duplicate, unpaid or error. |
filtered | boolean | Status rows: true when minSpreadPct dropped the property (not billed). |
error | string | Status rows: error message, null when the job succeeded. |
fetched_at | string | UTC timestamp of the fetch. |
Example row from a real run
Taken from the actor's README — this is the shape your dataset holds.
{ "type": "parity", "query": "https://www.google.com/travel/hotels/entity/ChkIhLCQwvjO2IYWGg0vZy8xMXE0bTZieDkyEAE", "hotel_name": "Brown's | Avenue Hotel", "entity_id": "ChkIhLCQwvjO2IYWGg0vZy8xMXE0bTZieDkyEAE", "hotel_id": null, "hotel_url": "https://www.google.com/travel/hotels/entity/ChkIhLCQwvjO2IYWGg0vZy8xMXE0bTZieDkyEAE", "stars": null, "rating": null, "reviews": null,
"check_in": "2026-09-27", "check_out": "2026-09-29", "nights": 2, "adults": 2, "children": 0, "currency": "USD",
"sources": [
{ "source": "Vio.com", "source_id": 2017, "official": false, "price_nightly": 377.39, "price_total": 754.78, "free_cancellation": null, "url": "https://www.vio.com/…" },
{ "source": "Brown's Avenue", "source_id": 12058, "official": true, "price_nightly": 488.46, "price_total": 976.92, "free_cancellation": null, "url": "https://be.synxis.com/?hotel=…&arrive=2026-09-27&depart=2026-09-29" },
{ "source": "Booking.com", "source_id": 184, "official": false, "price_nightly": 563.3, "price_total": 1126.61, "free_cancellation": null, "url": "https://www.booking.com/hotel/pt/browns-avenue.html?checkin=2026-09-27&checkout=2026-09-29" },
{ "source": "Expedia.com", "source_id": 232, "official": false, "price_nightly": 573.94, "price_total": 1147.88, "free_cancellation": null, "url": "https://www.expedia.com/…" }
],
"n_sources": 4, "min_source": "Vio.com", "min_price": 377.39, "max_source": "Expedia.com", "max_price": 573.94, "spread_pct": 52.1, "median_price": 525.88,
"official_source": "Brown's Avenue", "official_price": 488.46, "official_is_cheapest": false, "undercut_by": ["Vio.com"], "undercut_pct": 22.7,
"fetched_at": "2026-08-29T06:15:42+00:00" }
{ "type": "status", "query": "Brown's Avenue Hotel Lisbon", "hotel_name": "Brown's | Avenue Hotel", "entity_id": "ChkIhLCQwvjO2IYWGg0vZy8xMXE0bTZieDkyEAE", "check_in": "2026-09-27", "check_out": "2026-09-29", "status": "ok", "n_sources": 4, "spread_pct": 52.1, "filtered": false, "error": null, "fetched_at": "2026-08-29T06:15:42+00:00" }
Hotel rate parity in Python, JavaScript and curl
Python
from apify_client import ApifyClient
client = ApifyClient("<TOKEN>")
run = client.actor("kestrel/hotel-rate-parity").call(run_input={
"hotelNames": ["Hyatt Regency Lisbon", "Altis Grand Hotel Lisbon"], "checkIn": "30 days", "nights": 2, "sweepDays": 6, "currency": "EUR", "country": "pt"})
rows = [r for r in client.dataset(run["defaultDatasetId"]).iterate_items() if r["type"] == "parity"]
for r in sorted(rows, key=lambda r: -(r["undercut_pct"] or 0)):
if r["undercut_by"]: print(r["hotel_name"], r["check_in"], f'{r["undercut_pct"]}% under direct via {r["undercut_by"][0]} ({r["min_price"]} vs {r["official_price"]} {r["currency"]})')
JavaScript (Node)
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: '<TOKEN>' });
const run = await client.actor('kestrel/hotel-rate-parity').call({ hotels: ['ChkIhLCQwvjO2IYWGg0vZy8xMXE0bTZieDkyEAE'], checkIn: '30 days', nights: 1, minSpreadPct: 5 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
const lines = items.filter(r => r.type === 'parity' && r.undercut_by.length).map(r => `${r.hotel_name} ${r.check_in}: ${r.min_source} ${r.min_price} vs direct ${r.official_price} (${r.undercut_pct}%)`);
console.log(lines.join('\n') || 'in parity');
curl
curl -X POST "https://api.apify.com/v2/acts/kestrel~hotel-rate-parity/run-sync-get-dataset-items?token=<TOKEN>" \
-H "Content-Type: application/json" \
-d '{"hotels": ["ChkIg-b2ismUj7M1Gg0vZy8xMWg3MThreGg1EAE"], "checkIn": "30 days", "nights": 1, "currency": "USD"}'
The same three calls are runnable files in the examples repository: examples/hotel-rate-parity.py|.js|.sh.
Limits and honest notes
- Rates are what Google Hotels displays for the market (
country) and currency you choose. Whether taxes and fees are included follows Google's display rules for that market; a parity comparison across markets is not like for like. - Sources per property vary by market and day (typically 3–25 with US settings). A property with a rate on only one source still gets a row (spread 0); a property with none gets a free
no_ratesstatus. - Official-site detection is Google's. A few properties are listed under a marketplace brand (a property manager, a franchise engine) and carry no official flag; the row then has
official_price: null. - Google's comparison shows each source's lowest offer for the occupancy. Room-level parity (the same room type on every channel) is a different question: use the parent scraper with
offerLevel: "rooms"for that. - Name lookups are guarded, not clairvoyant: a name matching a different property with the same words (two "Grand Hotel" in one city) resolves to Google's best match. Check the
entity_idonce, then switch tohotels. - Google changes its internal formats occasionally; a daily canary run watches for that, the actor is rebuilt within days, and every run reports
statusrows so a failure is visible rather than a silent zero.
FAQ
Does it need a Google Hotels API key or a Google account?
No. There is no public API for reading Google Hotels prices; the actor reads the public results through Apify's proxies. No login, cookies or API key.
What is rate parity, in the terms this actor uses?
Rate parity means the same room sells for the same price on every channel, including your own site. The row measures it two ways: spread_pct is the gap between the cheapest and dearest source across the market, and undercut_pct is how far the cheapest channel beats your direct rate. A rate parity check is a scan of undercut_by; a hotel rate audit is that column over time, and official_price is your direct booking rate as Google shows it. Google Hotels rate parity, in short, is what a guest comparing channels on Google actually sees.
Can it run a rate parity check for my whole comp set every morning?
Yes: list the properties in hotels (or hotelNames for the first run), use a relative checkIn, add sweepDays for the booking window, and schedule the actor in Apify Console. Every run appends dated rows; minSpreadPct keeps the feed to breaches.
Why is the OTA cheaper than my official site?
Usually a wholesale or bed-bank rate resold on a metasearch-facing OTA, a member price shown to everyone by a listing site, or a stale rate on a channel you stopped updating. The sources array names the exact source and gives its booking link, so the trail starts from the row.
Which booking sites are compared?
Every source Google Hotels lists for that property and market — the big OTAs, metasearch resellers and regional sites — plus the official site when Google identifies one. Names and source_ids come straight from Google.
Does the price include taxes and fees?
It is the nightly and total figure Google Hotels shows for your market. Some markets display all-in prices, others add taxes later; set country to the market you sell in so every source follows the same rule.
How is this different from the Google Hotels Prices Scraper?
Same data, different shape. The parent returns one row per source (and can go to room level); this one returns one row per property and stay with the comparison and the parity math done, filters on spread before billing, and accepts names. Use the parent when you need every source as its own row or room-level offers.
Is there a free tier?
Yes: Apify's free plan covers small runs, and a run that delivers no parity rows costs nothing.
Last verified working: 2026-08-29.
How to use Hotel Rate Parity Checker
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 run | What it does |
|---|---|
| Daily Parity Violation Report for Hotels | Four Lisbon properties checked across a week of check-in dates: one row each with every source rate, the spread and which channel broke parity. |
| Direct Booking Rate Undercut Check | Is your own site the cheapest place to book? Fourteen check-in dates for one hotel, each row naming the channel that undercuts the direct rate. |
| Competitor Rate Shopping Without a Seat | Five Barcelona competitors, one night, every booking source side by side - the rate shop a revenue manager runs, without a per-seat SaaS contract. |
| Rate Parity Check for a Single Hotel | The smallest possible run: one hotel by name, one night, every source rate and the parity spread in a single row for one cent. |
| Rate Parity Monitoring Across 30 Days | One property, thirty consecutive check-in dates: where parity holds near-in and where it breaks further out along the booking curve. |
| Rate Parity Checker: Spreads Over 10% | minSpreadPct 10 delivers only the Paris properties whose cheapest and dearest source differ by more than a tenth - the rest cost nothing. |
| Hotel Rate Audit on the German Market | Berlin properties audited on the German Google market in EUR: HRS and German OTAs appear in the source list that the US market never shows. |
| Rate Shopping Tool for a Hotel Group | Eight properties across eight European cities in one run: a group-level parity sweep that fits in a single scheduled job and a single dataset. |
| OTA Price Comparison for Weekend Stays | Two-night stays starting on each of seven consecutive days across four Amsterdam hotels: where the weekend break is priced out of line. |
| Hotel Price Comparison at Family Occupancy | Two adults and children aged 5 and 10 on a three-night Orlando stay: extra-person rules differ by channel, so parity at family occupancy differs too. |
| Hotel Rate Shopper for the UK Market | Five London hotels shopped on the UK Google market in GBP, where the source list and the tax treatment differ from the US answer. |
| Google Hotels Rate Parity by Entity URL | Paste Google Hotels URLs instead of names: exact properties, no place search, no ambiguity between two hotels with similar names. |
Related scrapers
Run Hotel Rate Parity Checker
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/hotel-rate-parity