Hotel Rate Parity Checker — OTA Price Comparison avatar

Hotel Rate Parity Checker — OTA Price Comparison

Pricing

$10.00 / 1,000 parity rows

Go to Apify Store
Hotel Rate Parity Checker — OTA Price Comparison

Hotel Rate Parity Checker — OTA Price Comparison

Hotel rate parity checker on Google Hotels prices: one row per property and stay with every OTA and the official site side by side (Booking.com, Expedia, Agoda, Hotels.com, Trip.com), the cheapest source, the spread and how far direct is undercut. Rate shopping for revenue managers. Pay per row.

Pricing

$10.00 / 1,000 parity rows

Rating

0.0

(0)

Developer

Tedj MEABIOU

Tedj MEABIOU

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 hours ago

Last modified

Share

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.

You pay only for delivered parity rows. Properties with no rates, names that do not resolve, duplicates, rows a filter removed and status rows cost nothing.

What does the rate parity checker return?

Google Hotels is a metasearch engine: for a property and a stay it shows the rate of every booking site it knows and the property's own site. The parent scraper of this actor returns that comparison as one row per source. This actor collapses it into what a parity check actually needs — one parity row per property and stay:

  • sources — every source that carried a rate, cheapest first, each with its name, Google's source_id, an official flag, the nightly rate, the stay total, the free-cancellation flag and the booking link with your dates filled in (official sites: the booking engine URL).
  • min_source / min_price and max_source / max_price — who is cheapest, who is dearest, and spread_pct between them. median_price shows where the market sits.
  • official_source / official_price — the property's own listing when Google shows one, official_is_cheapest, the list of OTAs in undercut_by, and undercut_pct: how far the cheapest channel beats direct.
  • Identity and context: hotel_name, entity_id, hotel_url, check_in, check_out, nights, adults, children, currency, fetched_at; for name lookups also hotel_id, stars, rating and reviews.

A status row per property and stay (never charged) says what happened: ok, filtered, no_rates, not_found, duplicate, unpaid or error.

Rate parity monitoring on a schedule

Parity breaks quietly: a wholesaler feeds a bed bank, a bed bank feeds a metasearch, and by Friday an OTA is 8% under your direct rate for the weekend you were sure of. A rate parity checker is only useful if it runs every day, so this actor is built for schedules:

  • Relative dates. "checkIn": "30 days" means "the stay 30 days from whenever this run starts", forever. No stale dates, no edited schedules.
  • A window per run. sweepDays: 6 checks the same length of stay on seven consecutive check-in dates — one parity row each — so a single scheduled run covers the week that matters for your pickup.
  • Violations only. minSpreadPct: 5 delivers only properties whose cheapest-to-dearest spread reaches 5%; everything in parity gets a free status row. Your alert channel sees breaches, not noise, and you are not billed for the properties that behaved.
  • History you own. Every run appends dated rows to the same dataset. Export to Sheets, BigQuery or a warehouse and the undercut_pct column over time is your parity violation report — and, read across properties, plain hotel price tracking and hotel price comparison for the set you named.

Pair the run with Apify's webhooks or an n8n, Make or Zapier flow and a breach becomes a Slack message with the property, the OTA, the gap and the booking link — see the integrations section below.

OTA price comparison: the sources Google lists

Whatever Google Hotels shows for that property and market is what you get: Booking.com, Expedia, Hotels.com, Agoda, Trip.com, Priceline, Vio.com, Etrip, Super.com, KAYAK, Kiwi.com, eDreams, Bluepillow, Hostelworld, Vrbo, Holidu and dozens of regional OTAs, plus the official site. Two details matter for a clean OTA rate comparison:

  • Sources are keyed by Google's source_id, not by name. A brand can appear twice for one property — its official listing (official: true) and its marketplace rate — and both are kept as separate entries in sources.
  • Rates are the lowest rate each source offers for your occupancy and dates, in one currency, for one market. Set country to the market you sell in, because sources, prices and whether taxes are included differ by market, and compare like with like.

The official site is identified by Google itself, not guessed from the name. When Google lists no official site the direct fields are null and the row still gives you the full OTA price comparison.

Hotel price monitoring for your competitive set

A revenue manager's comp set is a list of names, not tokens. hotelNames accepts names — "Hyatt Regency Lisbon", "Brown's Avenue Hotel Lisbon" — and resolves each with 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. Google answers every search with something, so this guard is what stops a typo from turning into a paid row about a stranger. Name lookups also carry stars, rating, reviews and hotel_id from the listing, which URL lookups do not.

Once resolved, reuse the entity_id from the row in hotels: exact, one call per stay, and no search at all. That is the cheapest and most reliable way to run the same comp set every morning, and it is how hotel price monitoring should be wired after the first run.

Input

FieldMeaning
hotelsGoogle Hotels URLs (…/travel/hotels/entity/ChkI…) or bare entity tokens. Exact: one offers call per property and stay, no search, nothing unrelated billed.
hotelNamesProperty names. One place search each; kept only when the listing's name matches most of your words (at least two). Add the city to a generic name.
checkInYYYY-MM-DD, or relative for schedules: "30 days", "6 weeks", "+30d", today, tomorrow. Past check-ins are rejected before anything is fetched.
nightsLength of stay, 1–30. Check-out = check-in + nights.
sweepDaysAlso check the same stay starting on each of the next N days (a week of check-in dates = 6).
adults, childrenAgesOccupancy per room; rates scale with it exactly as on Google.
currency, language, countryOne currency for every price; Google UI language (hl); market (gl) — decides sources and tax display.
minSpreadPct0 = every property. N = only properties whose spread is at least N% (violations-only feed). Dropped properties are never billed.
proxyConfigurationApify datacenter proxies work; keep the default.
sessions, perIpParallel proxy sessions and requests per second per IP (defaults 4 and 0.5).

Example: two properties, tonight plus 30 days, one night

{ "hotels": ["https://www.google.com/travel/hotels/entity/ChkIhLCQwvjO2IYWGg0vZy8xMXE0bTZieDkyEAE", "ChkIg-b2ismUj7M1Gg0vZy8xMWg3MThreGg1EAE"], "checkIn": "30 days", "nights": 1, "adults": 2, "currency": "USD" }

Example: a comp set by name, a week of check-in dates, breaches only

{ "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" }

Example: a family occupancy in the market you sell in

{ "hotels": ["ChkIhLCQwvjO2IYWGg0vZy8xMXE0bTZieDkyEAE"], "checkIn": "2026-10-16", "nights": 3, "adults": 2, "childrenAges": [5, 9], "currency": "GBP", "country": "gb", "language": "en" }

Output

parity row (the Console's Rate parity and Undercut check views show the same rows as tables; the dataset schema documents every field):

{ "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" }

status row (never charged):

{ "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" }

A run also writes a SUMMARY record to its key-value store: parity rows delivered, how many undercut their official site, filtered, no rates, not found, duplicates, errors, charged events and HTTP stats.

How is the parity math calculated?

Everything is computed from the nightly rate of each source's lowest offer for your occupancy (when a source shows only a stay total, nightly = total ÷ nights). Using the row above:

  • Spread: (max − min) ÷ min × 100 = (573.94 − 377.39) ÷ 377.39 × 100 = 52.1%. This is the size of the gap across the whole distribution, official site included, and it is what minSpreadPct filters on.
  • Official is cheapest: the official rate is 488.46 and the cheapest source is 377.39, so false. A tie counts as parity (half a cent tolerance), and an official rate that is the minimum makes undercut_by empty and undercut_pct 0.
  • Undercut by: every non-official source below 488.46 — here only Vio.com. Cheapest first, so the first name is your worst offender.
  • Undercut %: (official − min) ÷ official × 100 = (488.46 − 377.39) ÷ 488.46 × 100 = 22.7%: the discount a guest gets for not booking direct.
  • Median: the middle of the four nightly rates, 525.88 — a quick read on whether one outlier or the whole market has moved.

When Google lists no official site, official_price, official_is_cheapest and undercut_pct are null and undercut_by is empty; spread, min, max and median still describe the OTA market.

How much does it cost?

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.

Google Hotels prices vs rate shopping tools: how this compares

  • Rate shopping tools for hotels — the hotel rate shopper and parity monitor SaaS sold per property per month, the competitor rate shopping and hotel price intelligence suites — shop the same public sources, add dashboards and alerts, and charge a seat or a property licence. This actor gives you the rows and the math for cents, and the dashboard is whatever you already use — a sheet, Metabase, Looker, a Slack channel.
  • SERP APIs (SerpApi, DataForSEO, Bright Data) return Google's search page per query; per-source rates are usually a second product, there is no parity math, and you pay per search whether or not it had results.
  • Channel managers know your own rates, not the OTAs' displayed prices after wholesaler and bed-bank markups. A channel manager rate check against what a guest actually sees needs a metasearch read, which is what this is.
  • Building it yourself: Google Hotels renders prices through internal RPC calls, not HTML, and the payload changes. The point of this actor is that someone keeps that working and reports failures as status rows instead of silent zeros.

There is no public Google Hotels API for reading prices; Google's own hotel APIs are feeds for properties that send rates to Google. Reading them back is a scraper, and this one is watched by a daily canary run.

How to use it from Python, JavaScript, curl, n8n, Make or an AI agent

Run it from the Apify Console with the form, or programmatically. Replace <TOKEN> with your Apify API token.

Python — daily breaches to stdout:

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.js — violations only, straight into a message:

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"}'
  • n8n, Make, Zapier: a Schedule trigger, an HTTP Request (or the Apify node) calling run-sync-get-dataset-items with the input above, a filter on undercut_by not empty, and a Slack, email or Sheets node — four nodes for a parity alert.
  • AI agents and MCP: the actor is available through the Apify MCP server and Apify AI; the README, input schema and flat rows are written so an agent can answer "is anyone undercutting our direct rate for next weekend?" from one run.
  • Google Sheets, BigQuery, Airtable: export the dataset from the run page or through Apify's integrations; sources is a nested array, so flatten it or keep the summary columns.

The actor reads publicly displayed prices — the same figures any visitor sees without logging in — and stores no personal data: rows describe properties, booking sources and rates, not people. Comparing public rates across channels is standard practice in the industry (it is what every rate shopping tool does), but terms of service and local law differ; check that your use complies, and use the data for your own analysis rather than republishing it as Google's.

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_rates status.
  • 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_id once, then switch to hotels.
  • 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.

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.

Rate parity is one question about a property. These answer the others, with the same pay-per-delivered-row billing and the same scheduling story:

All of them bill per delivered row, never charge for rows a filter or a spending limit removed, and write an Apify dataset you can export to CSV, Excel or JSON.