Skip to content

Compare OTA rates for one night

Get Booking.com, Hotels.com, Agoda and Expedia prices for the same stay in one call, or every seller Google shows.

Overview

You get
The nightly price for the same stay on Booking.com, Hotels.com, Agoda and Expedia, the lowest source, the spread and the basis of each price.
Calls
One name search per OTA, once per hotel, then POST/v1/ota/compare per stay
Cost
1 credit per name search (once) + 20 credits per comparison
Latency
Several seconds to tens of seconds: each OTA is queried live, in parallel

Collect the OTA ids

Each OTA identifies the hotel differently. Resolve them once and store them next to your hotel record:

OTAEndpointRead
Booking.com
recommended_match.pagename and .country
Hotels.com
recommended_match.property_id
Agoda
recommended_match.property_id
Expedia
recommended_match.property_id
import os
import requests

API = "https://api.scrapercompany.com"
HEADERS = {"x-api-key": os.environ["SCRAPERCOMPANY_API_KEY"]}
hotel = {"name": "Moxy Boston Downtown", "city": "Boston"}

def resolve(path, body):
    r = requests.post(f"{API}{path}", headers=HEADERS, json=body, timeout=90)
    r.raise_for_status()
    data = r.json()
    match = data["recommended_match"]  # None unless one candidate is a confident match
    if match is None:
        print(path, data["search_status"], [m["name"] for m in data["matches"]])
    return match

booking = resolve("/v1/ota/booking/search", hotel)
hotels = resolve("/v1/ota/hotels/search", {**hotel, "market": "US"})
agoda = resolve("/v1/ota/agoda/search", hotel)
expedia = resolve("/v1/ota/expedia/search", {**hotel, "market": "US"})

ids = {
    "booking_pagename": booking and booking["pagename"],
    "booking_country": booking and booking["country"],
    "hotels_property_id": hotels and hotels["property_id"],
    "agoda_property_id": agoda and agoda["property_id"],
    "expedia_property_id": expedia and expedia["property_id"],
}
print(ids)

When recommended_match is null

The search found only weak or ambiguous candidates (search_status is ambiguous or not_found). Look at matches[] and their match_score/confidence, or add a city, and store the id you confirm.

Compare in one call

Pass whichever ids you have — at least one is required. Sources run in parallel and fail independently: one OTA timing out does not fail the others.

curl -X POST "https://api.scrapercompany.com/v1/ota/compare" \
  -H "x-api-key: $SCRAPERCOMPANY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "check_in": "2026-11-30",
    "nights": 1,
    "currency": "USD",
    "booking_pagename": "moxy-boston-downtown",
    "booking_country": "us",
    "hotels_property_id": "38766175",
    "hotels_market": "US",
    "agoda_property_id": "8795952",
    "expedia_property_id": "38766175",
    "expedia_market": "US"
  }'
Response (shortened)
{
  "comparison": {
    "status": "comparable",
    "comparable": true,
    "reason": "Sources price on different bases (booking_display_average, stay_headline, total_including_taxes_and_fees); the headline minimum mixes them. Compare within `by_price_basis`.",
    "lowest_price": 285,
    "lowest_source": "booking",
    "spread": 59,
    "prices": {
      "booking": 285,
      "hotels": 301,
      "agoda": 332,
      "expedia": 344
    },
    "currencies": {
      "booking": "USD",
      "hotels": "USD",
      "agoda": "USD",
      "expedia": "USD"
    },
    "price_basis_consistent": false,
    "by_price_basis": {
      "booking_display_average": {
        "sources": [
          "booking"
        ],
        "currencies": [
          "USD"
        ],
        "lowest_source": "booking",
        "lowest_price_per_night": 285
      },
      "stay_headline": {
        "sources": [
          "hotels"
        ],
        "currencies": [
          "USD"
        ],
        "lowest_source": "hotels",
        "lowest_price_per_night": 301
      },
      "total_including_taxes_and_fees": {
        "sources": [
          "agoda",
          "expedia"
        ],
        "currencies": [
          "USD"
        ],
        "lowest_source": "agoda",
        "lowest_price_per_night": 332
      }
    }
  },
  "sources": {
    "booking": {
      "status": "ok",
      "price_per_night": 285,
      "currency": "USD",
      "price_basis": "booking_display_average"
    },
    "hotels": {
      "status": "ok",
      "price_per_night": 301,
      "currency": "USD",
      "price_basis": "stay_headline"
    },
    "agoda": {
      "status": "ok",
      "price_per_night": 332,
      "currency": "USD",
      "price_basis": "total_including_taxes_and_fees"
    },
    "expedia": {
      "status": "ok",
      "price_per_night": 344,
      "currency": "USD",
      "price_basis": "total_including_taxes_and_fees"
    }
  },
  "meta": {
    "requested": 4,
    "ok": 4,
    "elapsed_s": 15.89
  }
}
Each sources entry also carries the full payload of that OTA's own endpoint.

No price, no charge

If none of the sources returns a price, the comparison is not charged.

Alternative: Google offers

POST/v1/offers (3 credits) returns every seller Google shows for one night — OTAs and the hotel's own site — with per-room prices, from a single Google property token. It is cheaper and needs no OTA ids, but it reflects what Google displays rather than each OTA's own inventory. Use compare when you need the OTAs' own numbers; use offers for a quick market view.

Checking parity

  • Compare like with like: currency applies to Booking.com and Agoda, while Hotels.com and Expedia price in the currency of their point of sale (hotels_market, expedia_market). Check comparison.comparable and currencies before comparing numbers, and keep the markets constant between runs.
  • Sources also price on different bases: Booking.com's display average, Hotels.com's stay headline, a tax-inclusive total on Agoda and Expedia. Each priced source reports its price_basis. When comparison.price_basis_consistent is false, the headline lowest_price mixes bases (and reason says so): compare within comparison.by_price_basis, which groups the sources that share a basis with their own lowest price.
  • Expedia's compared price is its stay total divided by the nights, because its own nightly figure is before taxes. For its rooms and rate plans, use Expedia rates.
  • Add the hotel's own price with official-site rates to see whether an OTA undercuts the direct channel.
  • Tripadvisor's metasearch offers (POST/v1/ota/tripadvisor) need a location_id taken from the hotel's Tripadvisor URL (the digits after -d); its name search is not available yet.