# Hotel Rate Parity Monitor: Who Undercuts Your Site (`aleksandrbooking/google-hotels-rate-parity-monitor`) Actor

Check a hotel's rate parity on Google Hotels, date by date: which booking sites charge less than the hotel's own site, by how much, and the dates on which the hotel's own site is not listed. Scorecards per hotel and per site. Not affiliated with Google.

- **URL**: https://apify.com/aleksandrbooking/google-hotels-rate-parity-monitor.md
- **Developed by:** [Aleksandr Kobtsev](https://apify.com/aleksandrbooking) (community)
- **Categories:** Travel, Marketing, Automation
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.80 / 1,000 checked dates

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

Check a hotel's rate parity on Google Hotels, date by date: what the hotel's own site charges, which booking sites charge less, by how much, and the dates on which the hotel's own site is not listed at all. One dataset row per hotel and check-in date, a scorecard for each hotel and for each booking site, the list of breaches with the biggest gaps first, and an alert when a site is well below the hotel's own price.

There is no login, no browser and no partner API. Prices come from Google Hotels pages that anyone can open, read as plain HTML. The Actor requests only the `/travel/hotels/` pages Google's robots.txt leaves open, and says in every record what it could and could not read.

### Who is this rate parity monitor for?

| You are | What you run | What you get |
|---|---|---|
| A hotel owner or revenue manager | your hotel, the next two weeks, every day | the dates on which a booking site is below your own price, which site, how far, and whether you are listed on Google with your own site at all |
| A hospitality marketing or revenue agency | the hotels of your clients, weekly, one run per client | a scorecard for each hotel and a list of breaches you can send to the booking site's account manager |
| A hotel chain or a management company | the portfolio, a few dates a week | the hotels that are undercut most often and the sites that do it, ranked |
| A channel manager or a metasearch consultant | a city, one weekend | the market's parity at a glance: how many hotels are undercut and by whom |

### What makes this different from a price scraper?

- **It starts from the hotel's own price.** For every date it finds the hotel's own site among the sites Google lists, and compares every other site with it. A scraper gives you a list of prices; this gives you the verdict and the gap.
- **It says when the hotel's own site is not listed.** On a date for which Google lists booking sites and not the hotel's own, a guest cannot book direct from Google's page. That is a finding of its own, not a missing price.
- **It scores hotels and sites.** HOTEL_SCORECARDS gives each hotel its share of dates in parity, undercut and not listed, its median and worst gap and the sites that undercut it most. SITE_SCORECARDS ranks the booking sites over the whole run.
- **The country of sale is a setting.** Google lists different sites at different prices for guests of different countries; run the same hotels for the markets you sell to.
- **A date with no price at all is free.** It is delivered, marked sold out, and not charged.

### What data does it return?

One row per hotel and check-in date, in calendar order, hotel by hotel.

| Field | What it is |
|---|---|
| **name**, **hotelClass**, **rating**, **reviewsCount** | The hotel, its stars (`null` where Google does not class it), Google's guest rating of 5 and the number of reviews. |
| **city**, **countryCode**, **website** | Where it is and its own website as Google lists it. |
| **googleUrl**, **googleMid**, **entityToken** | The hotel's Google Hotels page for this stay, and Google's own identifiers: the map feature id (`null` for vacation rentals) and the token in the page address. Stable, so they are the keys to join runs on. |
| **checkIn**, **checkOut**, **nights**, **dayOfWeek**, **adults**, **currency**, **country** | The stay: check-in and check-out dates (YYYY-MM-DD), the length, the weekday of check-in (`Mon`, `Tue` ...), the guests, the currency of the prices and the country of sale they are for. |
| **available** | `false` when no booking site has a price for the hotel for this stay: the date is sold out or not bookable. Such a row is delivered and not charged. |
| **parityStatus** | `parity` when no site is the asked percent or more below the hotel's own site, `undercut` when one is, `no_official_offer` when the hotel's own site is not among the sites Google lists a price for; `null` without a price. |
| **ownPrice**, **ownSite** | The hotel's own price per night, as Google shows it (with taxes and fees where Google splits them out), and the name Google gives its site. `null` when it is not listed. |
| **cheapestSite**, **cheapestPrice** | The booking site with the lowest price and that price, per night. |
| **worstGapPercent**, **breachCount** | How far below the hotel's own price the cheapest breaching site is, in percent, and how many sites are at least the asked percent below it. |
| **breaches** | The sites that undercut the hotel's own site, the biggest gap first: `site`, `price`, `percentBelow`, `freeCancellation` and `bookingUrl` (the booking site's own page for this hotel and stay). |
| **sitesCount** | How many booking sites have a price for the hotel on the date. |
| **alerts** | Why the row is on the ALERTS list: `undercut`, `no_official_offer` (below). |
| **query**, **source**, **scrapedAt** | The hotel name or search text the row came from, whether it was a `search`, a `name` or a `link`, and when it was read (ISO-8601, UTC). |
| **customData** | Your own JSON from the input, copied onto every row. Present only when you give it. |

An example row, as a run returned it on 11 October 2026: a hotel in Batumi, sold in the United States, for one night. It had twelve sites below its own; three are shown, and the links in it are shortened.

```json
{
  "name": "Best Western Premier Batumi",
  "hotelClass": 5,
  "rating": 4.3,
  "reviewsCount": 1056,
  "city": "Batumi",
  "countryCode": "GE",
  "website": "https://www.bestwestern.ru/ru_RU/book/hotel-details.91457.html?aff=BRU&i...",
  "googleUrl": "https://www.google.com/travel/hotels/entity/ChkIhOLMzo-nnJ1fGg0vZy8xMWdu...",
  "googleMid": "0x406787b5000b26db:0x5f3a7138f9d33104",
  "entityToken": "ChkIhOLMzo-nnJ1fGg0vZy8xMWduamJjcmJsEAE",
  "checkIn": "2026-11-01",
  "checkOut": "2026-11-02",
  "nights": 1,
  "dayOfWeek": "Sun",
  "adults": 2,
  "currency": "USD",
  "country": "us",
  "available": true,
  "parityStatus": "undercut",
  "ownPrice": 101.2,
  "ownSite": "Best Western Premier Batumi",
  "cheapestSite": "Super.com",
  "cheapestPrice": 65.67,
  "worstGapPercent": 35.11,
  "breachCount": 12,
  "breaches": [
    { "site": "Super.com", "price": 65.67, "percentBelow": 35.11, "freeCancellation": false, "bookingUrl": "https://www.super.com/travel/transition/?data=price=53.83%26total_price=..." },
    { "site": "Reserving", "price": 69.14, "percentBelow": 31.68, "freeCancellation": false, "bookingUrl": "https://h-en.reserving.com/search?i=7103&rf=GOOGLEHOTELS&portal=rsvcom&l..." },
    { "site": "Reservation Day", "price": 70.66, "percentBelow": 30.18, "freeCancellation": false, "bookingUrl": "https://www.reservationday.com/transition/?data=price=59.88%26total_pric..." }
  ],
  "sitesCount": 24,
  "alerts": ["undercut"],
  "query": "hotels in Batumi",
  "source": "search",
  "scrapedAt": "2026-10-11T22:24:08.873Z"
}
```

#### The records of the run

Beside the dataset, the run writes these records to its key-value store.

**HOTEL_SCORECARDS**: one line per hotel over all its dates.

| Field | What it is |
|---|---|
| **name**, **entityToken**, **googleMid**, **googleUrl**, **website** | The hotel and its identifiers. |
| **dates**, **priced**, **soldOut** | Dates checked, dates with a price and dates without one. |
| **parity**, **undercut**, **noOfficialOffer** | Of the priced dates, how many are in parity, undercut and not listed with the hotel's own site. |
| **parityShare**, **undercutShare**, **noOfficialOfferShare** | The same as shares of the priced dates, 0 to 1; `null` when no date has a price. |
| **medianWorstGapPercent**, **maxWorstGapPercent**, **worstDate** | Over the dates the hotel was undercut: the median of the worst gap, the biggest worst gap and its date. |
| **topSites** | The sites that undercut the hotel most often, up to five: `site`, `dates`, `averageGapPercent`, `maxGapPercent`. |

**SITE_SCORECARDS**: one line per booking site that undercut a hotel in the run, the most frequent first: **site**, **breaches** (hotel dates on which it was below the hotel's own site), **hotels**, **averageGapPercent**, **maxGapPercent** and **shareOfUndercutDates** (the share of the run's undercut hotel dates it was among the sites below, 0 to 1).

**BREACHES**: every site below a hotel on a date, the biggest gaps first: **name**, **entityToken**, **checkIn**, **dayOfWeek**, **site**, **price**, **ownPrice**, **percentBelow**, **currency** and **url** (the hotel's Google page for the stay). At most 2,000 lines.

**DATE_SUMMARY**: the hotels of the run on each date: **checkIn**, **dayOfWeek**, **hotels**, **priced**, **soldOut**, **parity**, **undercut**, **noOfficialOffer**, **undercutShare** and **worstGapPercent**.

**ALERTS**: the rows that raised an alert, the earliest date first: **name**, **entityToken**, **checkIn**, **dayOfWeek**, **alerts**, **ownPrice**, **worstGapPercent**, **worstSite**, **cheapestSite**, **cheapestPrice**, **currency** and **url**.

**SUMMARY**: **hotels**, **dates**, **rowsRead**, **rowsDelivered**, **priced**, **soldOut**, **parity**, **undercut**, **noOfficialOffer**, **undercutShare**, **noOfficialOfferShare**, **alerts**, **charged**, **notCharged**, **googleRequests**, **googleFailedAttempts** and **failed**.

**FAILED_HOTELS**: the searches, names and dates that could not be read, each with `what`, `stage` and `reason`; absent when there were none.

### What are the alerts?

| Alert | The rule |
|---|---|
| `undercut` | A booking site is at least **Alert on a gap of at least** percent (10 by default) below the hotel's own price for the date. |
| `no_official_offer` | Google lists booking sites with a price for the hotel on the date and the hotel's own site is not among them. Can be switched off. |

### What did a real check show?

Measured on the platform on 12 October 2026: ten hotels of Batumi, thirty check-in dates starting two days ahead, one night, two adults, sold in the United States, a gap of two percent. 290 of the 300 hotel dates had a price.

- The hotel's own site was **undercut on 157 dates (54 percent)**, in parity on 46 (16 percent) and **not listed on 87 (30 percent)**.
- On the undercut dates the median of the worst gap was 22.7 percent and the biggest 61.3 percent; 151 dates had a gap of ten percent or more and 87 of twenty percent or more.
- The picture was by hotel, not by date. Five of the ten were undercut on every priced date but one or two, one on 12 of 30 and one on 2 of 30; three were not listed with their own site on any date. One hotel had 23 to 25 sites below its own on every date.
- The sites below the hotel's own most often were Reservation Day (115 hotel dates, four hotels, 20.6 percent on average), Super.com (96, six hotels, 21.2 percent), Etrip.net and Vio.com (66 each) and goseek.com (62).

How to read it. Google lists the price a guest sees on each site, including member, app-only and bundle rates and sites that resell the same room. A site below the hotel's own price is a fact about what a guest sees; whether it breaks a contract or is a rate the hotel allows is for the hotel to judge. A hotel that is not listed with its own site may simply not take part in Google's booking links.

### How do I use it?

1. Under **Hotels** put hotel names with the city (`Hilton Batumi`) or paste a Google Hotels link of a hotel. Or under **Searches** type what you would type into Google Hotels (`hotels in Batumi`) and set **Hotels per search**.
2. Set **Check-in dates to check** and **Days between check-ins**: 14 dates one day apart is the next two weeks, 13 dates a week apart is three months.
3. Set **Country of sale** to the market your guests come from; run again for the next market.
4. Press **Start**, or put the run on a schedule (daily or weekly) and read ALERTS and HOTEL_SCORECARDS after each run. An integration (Slack, e-mail, a webhook) on the run's success sends the records where you work.

From the API:

```json
{
  "hotels": ["Hilton Batumi", "Radisson Blu Batumi", "Le Meridien Batumi"],
  "daysAhead": 1,
  "dates": 14,
  "country": "us",
  "currency": "USD",
  "minGapPercent": 2,
  "alertGapPercent": 10
}
```

### What can I set?

| Setting | What it does |
|---|---|
| **Hotels** | Hotels by name with the city, or by Google Hotels link. |
| **Searches**, **Hotels per search** | Texts to search on Google Hotels, and how many of the hotels each lists to check, 1 to 200 (20 by default). |
| **First check-in date**, **Days ahead** | The first check-in: a date, or this many days from today (1 by default). A scheduled run with **Days ahead** always checks the same distance ahead. |
| **Check-in dates to check**, **Days between check-ins** | How many dates (1 to 120, 14 by default) and the gap between them (1 to 30 days). Every hotel is read once for every date. |
| **Nights** | The length of each stay, 1 to 30 (1 by default). |
| **Adults**, **Children (ages)** | The party the prices are for. |
| **Currency**, **Country of sale**, **Language** | The currency of the prices, the country Google sells the stay in (it decides which booking sites it lists and at what price) and the language of Google's texts. |
| **Smallest gap that counts (%)** | A site is a breach when it is at least this percent below the hotel's own site, 1 to 90 (2 by default). |
| **Alert on a gap of at least (%)** | The gap that raises `undercut`; 0 raises it for every breach (10 by default). |
| **Alert when the hotel's own site is not listed** | Raises `no_official_offer` (on by default). |
| **Custom data** | JSON copied onto every row, for telling clients or runs apart. |
| **Requests in parallel** | 1 to 10, five by default. |
| **Proxy** | Apify datacenter proxies by default; your own proxies if you choose Custom proxies. |

### What does it cost?

Pay per event. A `date` event for every hotel and check-in date delivered with a price: $0.0012 on the free plan, $0.00105 Bronze, $0.0009 Silver, $0.0008 Gold and above. A date with no price (sold out) is delivered and costs nothing. The platform's own events are the start of the run ($0.00005) and the dataset item ($0.00001) for every row stored.

Measured on the platform on 12 October 2026: ten hotels for thirty dates, 300 rows, took 5 minutes 18 seconds and used $0.0179 of platform usage. 290 of the rows had a price, so the run costs a buyer on the free plan 290 x $0.0012 + 300 x $0.00001 + $0.00005, about $0.35. Five hotels for ten dates took 59 seconds.

### Limits

- A run checks one stay length, one party and one country of sale. Other lengths, parties or markets are separate runs.
- Parity here is what Google shows for the stay at the moment of the request. A booking site may charge a different figure at checkout, and the same hotel shows different sites and prices in different countries.
- A hotel's page does not list the same booking sites in every answer: Google leaves some out, the more often the smaller the hotel. A hotel with few offers is read again, up to three times, and the offers are merged. How steady the figure is, measured on the same reading code: six hotels for ten dates were read three times on 11 October 2026 (165 pairs of reads of the same hotel and date). The cheapest price differed by more than half a percent in 4 pairs (2%), because a booking site listed in one answer was missing from the other; the Booking.com price and the hotel's own price did not differ in any pair. A breach seen on one date and one read is a lead, not a finding: read the same hotels on a schedule and look for the dates that come back.
- "Not listed" means that Google lists booking sites with a price and not the hotel's own. It does not say why.
- Google prices stays up to about eleven months ahead. A run delivers at most 20,000 rows.
- Google's pages change from time to time. The Actor finds hotels and offers by their shape and checks that every page answers for the stay it asked, so a change shows as a clear error rather than as wrong rows; if it ever does, open an issue with the run link.

### Is it legal to scrape Google Hotels prices?

This is an unofficial tool, not affiliated with, endorsed by or connected to Google LLC or any hotel or booking site. It reads publicly visible pages and collects no personal data. Google's terms restrict automated access, and you are responsible for making sure your use complies with those terms and with the law in your jurisdiction. See [Apify's stance on web scraping legality](https://blog.apify.com/is-web-scraping-legal/).

What the Actor does about robots.txt: it reads Google's robots.txt at the start of every run and requests only addresses it leaves open, which are the `/travel/hotels/` pages. It refuses any address the file closes.

### Found a problem?

Open an issue on the Issues tab with the run link and the hotel. Breakages in Google's pages are the expected failure mode of any scraper, and this one is built to say so rather than to deliver wrong data. Reports get read.

# Changelog

This Actor's version history is a separate document: https://apify.com/aleksandrbooking/google-hotels-rate-parity-monitor/changelog.md

# Actor input Schema

## `hotels` (type: `array`):

Hotels to check by name with the city ("Hilton Batumi") or by their Google Hotels link (the address that has /entity/ in it). A name that matches no hotel well enough is listed in FAILED_HOTELS and is not charged.

## `searchQueries` (type: `array`):

What you would type into Google Hotels: "hotels in Batumi", "boutique hotels in Lisbon". The first hotels Google lists for each text are checked; set how many under "Hotels per search".

## `maxHotelsPerSearch` (type: `integer`):

How many hotels of each search text to check, 1 to 200. Every hotel is read once for every check-in date, so twenty hotels for 14 dates is 280 rows.

## `checkInDate` (type: `string`):

YYYY-MM-DD. Leave empty to use "Days ahead" instead. Google prices stays up to about eleven months ahead.

## `daysAhead` (type: `integer`):

The first check-in is this many days from today, when no first check-in date is given. A scheduled run with this set always checks the same distance ahead.

## `dates` (type: `integer`):

How many check-in dates to check, starting with the first one. Each date is a stay of "Nights" nights and is one row for every hotel, charged when it has a price. 1 to 120. 14 dates for 10 hotels is 140 rows.

## `stepDays` (type: `integer`):

The gap between the check-in dates: 1 for every day, 7 for the same weekday every week.

## `nights` (type: `integer`):

How many nights each stay lasts. One night gives the rate of the arrival date.

## `adults` (type: `integer`):

Adults in the room. Prices are for the whole party, not per person.

## `childrenAges` (type: `array`):

One entry per child, the age in years: 4, 11. Up to five children.

## `currency` (type: `string`):

The currency prices are given in. Google converts for booking sites that charge in another currency.

## `country` (type: `string`):

The country Google sells the stay in (a two-letter code: us, gb, de, ge). It decides which booking sites Google lists and at what price, so parity is a statement about guests of this country. Check the countries your guests come from in separate runs.

## `language` (type: `string`):

The language of the texts Google returns: descriptions, labels, amenities.

## `minGapPercent` (type: `integer`):

A booking site is a breach when it charges at least this percent less than the hotel's own site for the same stay. 1 is the smallest; 2 is the default, which leaves out the rounding of prices that are the same in effect.

## `alertGapPercent` (type: `integer`):

A date on which a site is at least this percent below the hotel's own raises the undercut alert. 0 raises it for every breach.

## `alertNoOfficialOffer` (type: `boolean`):

On: a date on which Google lists booking sites for the hotel but not the hotel's own site raises the no_official_offer alert. The hotel is then not bookable direct from Google for that date, whatever its prices.

## `customData` (type: `object`):

Any JSON. It is copied onto every row, so rows of different runs or clients can be told apart after merging.

## `concurrency` (type: `integer`):

How many requests run at the same time, across the whole run. The default is gentle on Google and quick enough.

## `proxy` (type: `object`):

Apify datacenter proxies are the default and what this Actor is priced on. Apify residential groups are not available here; a run that asks for one is refused in the first seconds, before any hotel is charged. For your own proxies choose Custom proxies and supply them.

## `dryRun` (type: `boolean`):

Produce data without charging events. Honored only for runs started by the Actor owner; ignored for everyone else.

## Actor input object example

```json
{
  "hotels": [
    "Hilton Batumi"
  ],
  "searchQueries": [],
  "maxHotelsPerSearch": 20,
  "daysAhead": 1,
  "dates": 14,
  "stepDays": 1,
  "nights": 1,
  "adults": 2,
  "childrenAges": [],
  "currency": "USD",
  "country": "us",
  "language": "en",
  "minGapPercent": 2,
  "alertGapPercent": 10,
  "alertNoOfficialOffer": true,
  "concurrency": 5,
  "proxy": {
    "useApifyProxy": true
  },
  "dryRun": false
}
```

# Actor output Schema

## `rows` (type: `string`):

No description

## `hotels` (type: `string`):

No description

## `sites` (type: `string`):

No description

## `breaches` (type: `string`):

No description

## `dates` (type: `string`):

No description

## `alerts` (type: `string`):

No description

## `summary` (type: `string`):

No description

## `failed` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "hotels": [
        "Hilton Batumi"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("aleksandrbooking/google-hotels-rate-parity-monitor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "hotels": ["Hilton Batumi"] }

# Run the Actor and wait for it to finish
run = client.actor("aleksandrbooking/google-hotels-rate-parity-monitor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "hotels": [
    "Hilton Batumi"
  ]
}' |
apify call aleksandrbooking/google-hotels-rate-parity-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,aleksandrbooking/google-hotels-rate-parity-monitor"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/itC4CHPB8uymy1N7S/builds/FK49gkwidgbPVpRQQ/openapi.json
