An anonymous scrape of an OTA returns the public rate. A signed-in user is very often shown a different, lower one. If your pipeline only collects the first, every number downstream of it is biased in the same direction — and you cannot see by how much.
This is not a scraping edge case. It is how the major online travel agencies are designed to work, and they publish the mechanics.
| Source | Programme | What the OTA states |
|---|---|---|
| Booking.com | Genius | Level 1 gives “10% discount on the price of your stay before taxes and charges” and requires you to sign in. Level 2, reached after 5 stays in 2 years, gives 10–15% plus breakfast and room upgrades on participating properties. (booking.com/genius.html) |
| MakeMyTrip | MMTBLACK | Gold and Platinum tiers both carry a “Min. 10% additional discount on select hotels”, and the programme terms state that you “must be logged in to your MMTBLACK account on MakeMyTrip while making the booking”. (MMTBLACK FAQs) |
| Agoda | Member prices | Agoda operates signed-in member pricing on top of its public rates. It does not publish a fixed tier-to-discount table, so the size of the gap has to be measured per property rather than assumed. |
A member rate is not a parameter in the URL. It is a property of the session. The OTA resolves the price against an authenticated account — its tier, its market, sometimes its booking history — and serves a page that a logged-out request will never be given, no matter how good the proxy is.
That has three consequences that matter when you are designing a collector:
A hotel compares its direct rate against OTA rates collected anonymously, sees parity, and concludes it is compliant. Meanwhile the signed-in guest is shown an OTA price below the direct rate. The parity report is clean and the actual undercutting is invisible.
Any model that learns competitor behaviour from anonymous prices learns a shifted distribution. Because the shift is uneven across properties and markets, it does not cancel out — it re-ranks which competitors look expensive.
An agent that quotes a price and then hands the user a booking URL creates an immediate, visible contradiction: the user lands on the page signed in and sees a different number. The failure is not silent, it is user-facing, and it destroys trust in the agent.
Teams reconciling Google Hotels against per-OTA scrapes routinely find the metasearch figure lower than every OTA price they collected, and write it off as staleness. Frequently it is not staleness — it is an authenticated or channel-specific rate they never captured.
You do not need a vendor to run this check. You need one afternoon and an account you already own.
Once you accept that a hotel price is not a single number, the fix is mostly schema discipline. Any row that does not answer all of the following is not comparable to any other row:
| Field | Why it is not optional |
|---|---|
authenticated | Whether this price came from a signed-in session at all. Without it, public and member rates silently pool into one series. |
member_price_confirmed | Signed in is not the same as served a member rate. Many properties show the member the same price. You need to distinguish "we were logged in" from "a member rate was actually applied". |
room_type | The single largest source of false price differences. Two rows for the same hotel and date are not comparable if they are different rooms. |
tax_inclusive / fee basis | Determines whether a 10% gap is real or an artefact of comparing pre-tax to post-tax. |
currency + locale | OTAs price by market. The same property on the same date has different numbers by point of sale, before any exchange rate is applied. |
captured_at | Rates move. A comparison across a multi-hour collection window is measuring time as much as it is measuring price. |
| Property identity | Covered separately — matching a hotel across OTAs by name is its own failure mode. Prefer a deterministic key; see Place ID lookup. |
Full disclosure, this is our product — but the mechanics are worth stating plainly because most vendors do not offer this at all.
The OTA Hotel Rates scraper prices a stay as a signed-in member on Agoda, Booking.com and MakeMyTrip. You send signed_in: true and we use one of the accounts provisioned for your key; alternatively you can supply your own credential with own_account and we scrape as that account instead. Every result carries the authenticated and member_price_confirmed provenance fields described above, so a member rate is never silently mixed into a public-rate series. Provisioned accounts are enabled per client.
For the comparison side, the Google Hotels scraper returns 12–25 OTAs per hotel with the property resolved by Google Place ID — deterministic, so you are not matching hotels on name strings — and batches up to 20 hotels per call. Full parameters and response schema are in the API docs; job times, rate limits and coverage are in the FAQ.
No. A coupon is applied at checkout to a price you can already see. A member rate changes the rate served to the page, and on some properties it changes which room types and supply channels are offered at all. You cannot reconstruct it by subtracting a percentage from the public price.
You can detect that a gap exists — a metasearch result persistently below every public OTA price you collect is a strong signal. You cannot measure it, because the discount varies by property, market, date and tier.
Agoda, Booking.com and MakeMyTrip. Results carry provenance fields so you know whether a row is a public rate or an authenticated one.
It depends on the property, date and tier, so any single headline number is misleading. Booking.com publishes its own floor: Genius Level 1 is 10% and Level 2 is 10–15%, before taxes and charges. Treat that as a lower bound on one source, not an industry average.