---
title: "Hotel Price Data for Rate Monitoring: Fields and Costs"
description: "The fields a rate-monitoring job needs from Booking, Expedia and Trivago, and what 200 hotels over 30 dates costs, 6,000 credits per source."
url: https://perceptlayer.com/blog/hotel-price-data-rate-monitoring
---

[Blog](https://perceptlayer.com/blog) / [Web data by industry](https://perceptlayer.com/blog/category/web-data)

# Hotel Price Data for Rate Monitoring: Fields and Costs

7 Sept 2026 Perceptlayer 8 min read

A rate-monitoring job needs one price per hotel, per stay date, per occupancy, in a known currency, tied to a room and a rate plan. Booking and Expedia room calls return room-level prices, and region searches on Booking, Expedia and Trivago return one price per hotel. Checking 200 hotels over 30 dates with room calls is 200 × 30 = 6,000 calls × 1 credit = 6,000 credits per source.

## Key takeaways

-   Pin the query: check-in, check-out, adults and currency decide what a price means, so store them next to every price.
-   Expedia `hotels-rooms` returns `rate_plans` per room with `total_price` and `price_per_night`; Booking `rooms` returns a `price` text string per room that you have to parse.
-   A region search prices hotels, a rooms call prices rooms. Use searches to find where prices moved and room calls to see which room moved.
-   200 hotels × 30 dates × 1 credit = 6,000 credits for one source and one occupancy. Failed calls cost nothing.
-   Total credits for a run is the sum of `X-Credits-Used`, not the difference between two `X-Credits-Remaining` readings.

## What does a rate-monitoring job need from a hotel price feed?

It needs five things on every row: the stay dates, the occupancy, the currency, the room or rate plan the price belongs to, and the source it came from. Without the first three, two prices can't be compared.

| Field | Why a rate check needs it | Where the recordings show it |
| --- | --- | --- |
| Check-in and check-out | A price is for a stay, not a night. Length of stay changes the rate. | Your request; Expedia and Trivago echo `check_in` and `check_out` in the response |
| Occupancy | Rates change with the number of adults. | Your request ( `group_adults`, `adults`); Booking rooms also list `occupancy` per room |
| Currency | Sources return different currencies for the same query. | Expedia `currency` ( `EUR`), Trivago `currency` ( `USD`), Booking inside the price text ( `$351.05`, `€ 588`) |
| Room type | Compares like with like across days. | Booking `name`, `occupancy`, `beds`; Expedia `unit_id`, `name`, `features` |
| Rate plan | The same room is sold at several prices. | Expedia `rate_plans[].rate_plan_id` |

One gap, stated once: neither room response includes cancellation terms. The only cancellation signal in the recordings is Expedia's hotel-level `free_cancellation` flag in search results, so if your rate comparison depends on refundable versus non-refundable rates, plan around that.

## What do the Booking, Expedia and Trivago responses actually contain?

Hotel-level search responses carry a name, a price, a rating and an identifier you can feed into the next call. Room-level responses carry the rooms and their prices for one hotel. The excerpts below are trimmed from recorded responses for 20 to 23 November 2026, three nights. The Booking and Expedia requests used two adults; the Trivago search request had no `adults` parameter.

### Hotel-level prices from region searches

The [Booking.com prices endpoint](https://perceptlayer.com/apis/booking/prices) returns hotels for a destination with a `price` string:

```json
"prices": [
  {
    "name": "Royal Saint Michel",
    "url": "https://www.booking.com/hotel/fr/royal-saint-michel.html",
    "price": "$351.05",
    "review_score": "8.8",
    …
  }
],
"total_prices": 25
```

The price is text with a currency symbol, and the response doesn't label whether it covers the stay or one night. Check that against the page before you compare it with anything.

The [Expedia hotels-search endpoint](https://perceptlayer.com/apis/expedia/hotels-search) returns numbers, a currency and both stay and nightly figures:

```json
{
  "property_id": "564660",
  "name": "Mercure Paris Centre Tour Eiffel",
  "total_price": 690,
  "price_per_night": 230,
  "free_cancellation": false,
  "sponsored": true,
  "currency": "EUR"
}
```

A discounted hotel in the same response also carries `original_price` (1329 against a `total_price` of 1203).

The [Trivago search endpoint](https://perceptlayer.com/apis/trivago/search) returns hotel records with `id`, `stars`, `rating`, `review_count` and `provider_count`, plus a `forecast_price` per night and a destination-wide `price_histogram`. `forecast_price` is a monthly forecast: it carries a `month` field ( `2026-11` and `2027-01` in the recording), so it is not the price for the dates you searched. The recorded hotels carry no per-hotel stay total, so treat Trivago search as a market-shape source. The [Trivago deals endpoint](https://perceptlayer.com/apis/trivago/deals) takes a hotel ID, dates, `rooms`, `children_ages[]` and `currency`; there's no recording of it here, so try it in the playground first.

### Room-level prices from rooms calls

The [Expedia hotels-rooms endpoint](https://perceptlayer.com/apis/expedia/hotels-rooms) returns each room with its rate plans:

```json
"currency": "EUR",
"rooms": [
  {
    "unit_id": "226385",
    "name": "Classic Room, 1 Double Bed",
    "features": ["18 sq m", "Sleeps 2"],
    "rate_plans": [
      { "rate_plan_id": "397104173", "total_price": 690, "price_per_night": 230 },
      { "rate_plan_id": "397104181", "total_price": 825, "price_per_night": 275 }
    ]
  },
  …
]
```

This is the same hotel as the first Expedia search result, and its cheapest rate plan (690) matches the search's `total_price`, so the rooms call shows what sits above the search price. Use `total_price` as the key figure: the second room's 700 over three nights is shown as 233 a night, so `price_per_night` is rounded.

The [Booking.com rooms call](https://perceptlayer.com/apis/booking) ( `/v1/booking/rooms`) returns a smaller record:

```json
{
  "name": "Attic Single Room",
  "price": "€ 978 € 588 Original price € 978 Current price € 588 € 326.12 × 3 nights € 978.35 Limited-time Deal …",
  "occupancy": "1 adult",
  "beds": "1 twin bed",
  "available": true
}
```

The price field is one text blob holding the original price, the current price and a deal note. Extract the number after "Current price" and keep the raw string. `occupancy` is the room's stated capacity, not an echo of your `group_adults`, so don't use it to verify your query.

## Should you monitor with region searches or room calls?

Use both, for different jobs. A region search answers "which hotels moved" at one call per page of results. A rooms call answers "which room and rate plan moved" for one hotel, at one call per hotel per date. They return different things, so a search's cost per date is not a cheaper way to get room prices.

|  | Region search | Rooms call |
| --- | --- | --- |
| Prices | One per hotel | One or more per room |
| Calls per date | One per page of results | One per hotel |
| Good for | Spotting a market-wide move, finding hotels to track | Rate-plan changes, room mix, undercut checks |
| Endpoints | Booking `prices`, Expedia `hotels-search`, Trivago `search` | Booking `rooms`, Expedia `hotels-rooms` |

A practical pattern: search across your 30 dates to see where the market moves, then run room calls for your own hotels and a fixed competitor set. More sources are on the [Travel APIs page](https://perceptlayer.com/verticals/travel).

## What does checking 200 hotels over 30 dates cost?

A full room sweep of 200 hotels over 30 check-in dates is 6,000 calls, and every successful call costs 1 credit, so 6,000 credits per source and occupancy. Failed calls cost nothing. What a credit costs is on the [pricing page](https://perceptlayer.com/pricing); the arithmetic below stays in credits.

| Job | Calls | Credits |
| --- | --- | --- |
| One room sweep, one source, 2 adults | 200 × 30 = 6,000 | 6,000 |
| The same sweep for 1 and 2 adults | 2 × 6,000 = 12,000 | 12,000 |
| Weekly sweeps for a month (4 runs) | 4 × 6,000 = 24,000 | 24,000, within the Build pack's 50,000 |
| Daily sweeps for a month (30 runs) | 30 × 6,000 = 180,000 | 180,000, within the Growth pack's 200,000 |
| Region search, 30 dates, first page only | 30 × 1 = 30 | 30 per city per source for the first page; a full city needs more calls |
| Free tier: 33 hotels × 30 dates | 990 | 990 of 1,000 free credits a month |

One page is not a whole city. In the recordings the first page returned 10 of Expedia's 500 results and 35 of Trivago's 2,500, so covering a city means more calls per date, following `offset` and `next_offset`.

Rate matters as much as credits. Paying accounts run at a minimum of 5 requests a second, so 6,000 calls take 6,000 ÷ 5 = 1,200 seconds, or 20 minutes, but only if you actually send 5 a second. That is a ceiling. Your throughput is workers ÷ latency: 4 workers at about 2 seconds a call give roughly 2 requests a second, which is about 50 minutes for 6,000 calls. Scale the worker count with latency, for example 10 workers at 2 seconds to approach the ceiling. Free accounts get 1 request a second. The [rate limits page](https://docs.perceptlayer.com/rate-limits) has the details, and the [credits page](https://docs.perceptlayer.com/credits) explains how balances are debited.

## How do you run the sweep and reconcile the credits?

Fan out the calls, honour `Retry-After`, and add up `X-Credits-Used` from every response. Don't compute the total from the first and last `X-Credits-Remaining` of a concurrent run: responses arrive out of order, so those two values don't bracket the run. This script sweeps Expedia room prices for a list of property IDs.

```python
import os
import time
from concurrent.futures import ThreadPoolExecutor
from datetime import date, timedelta

import requests

URL = "https://api.perceptlayer.com/v1/expedia/hotels-rooms"
HEADERS = {"X-API-Key": os.environ["API_KEY"]}
PROPERTY_IDS = ["564660", "994215"]  # your 200 Expedia property IDs
FIRST_CHECK_IN = date(2026, 11, 20)
DATES = 30
NIGHTS = 3
WORKERS = 4  # requests a second = WORKERS / latency in seconds, capped at 5; use 1 on a free account

def fetch(job):
    property_id, check_in = job
    params = {
        "property_id": property_id,
        "check_in": check_in.isoformat(),
        "check_out": (check_in + timedelta(days=NIGHTS)).isoformat(),
        "adults": 2,
    }
    for _ in range(3):
        try:
            r = requests.get(URL, params=params, headers=HEADERS, timeout=30)
        except requests.Timeout:
            # The call may have succeeded and been billed; the reply was lost.
            return job, [], 0, "timeout"
        if r.status_code in (429, 503):
            time.sleep(int(r.headers.get("Retry-After", "5")))
            continue
        break
    used = int(r.headers.get("X-Credits-Used", 0))
    if not r.ok:
        return job, [], used, r.json().get("error", {}).get("code", "error")
    data = r.json()["data"]
    rows = [
        {
            "property_id": property_id,
            "check_in": data["check_in"],
            "currency": data["currency"],
            "room": room["name"],
            "rate_plan_id": plan["rate_plan_id"],
            "total_price": plan["total_price"],
        }
        for room in data["rooms"]
        for plan in room["rate_plans"]
    ]
    return job, rows, used, None

jobs = [
    (pid, FIRST_CHECK_IN + timedelta(days=i))
    for pid in PROPERTY_IDS
    for i in range(DATES)
]
with ThreadPoolExecutor(max_workers=WORKERS) as pool:
    results = list(pool.map(fetch, jobs))

rows = [row for _, r, _, _ in results for row in r]
credits_used = sum(used for _, _, used, _ in results)
problems = [(job, why) for job, _, _, why in results if why]
print(f"{len(rows)} rate rows, {credits_used} credits used, {len(problems)} calls to review")
```

Because requests have no idempotency key, a timed-out call may have been billed even though you got no body. The script flags those instead of counting them as free. See [errors](https://docs.perceptlayer.com/errors) for the codes you'll hit, and the [Expedia API page](https://perceptlayer.com/apis/expedia) for the playground.

## What should you check before trusting the numbers?

Check four things before a price goes into a dashboard, so the series isn't a mix of different quantities.

1.  **Currency.** In the recordings Expedia returned EUR, Trivago USD and Booking a mix of `$` and `€` strings. Booking and Trivago accept a `currency` parameter and Expedia's room and search endpoints don't list one, so convert on your side and store the original currency.
2.  **Unit.** Expedia gives both `total_price` and `price_per_night`; use the total, since the nightly figure is rounded. For Booking text prices, confirm whether a value covers the stay or a night.
3.  **Identity.** Booking identifies hotels by `country` and `slug`, Expedia by `property_id`, Trivago by `id`. The recordings don't link them, so keep your own mapping table.
4.  **Timestamps.** Data is returned as found at the time of the call. Store the time you made each request next to the price.

Pick one source, run the sweep on 5 hotels and compare it with the site by hand before scaling to 200. [Get a free API key](https://perceptlayer.com/signup) for 1,000 credits a month; no card needed. For the Booking side in more depth, see [Booking.com Hotel Prices API](https://perceptlayer.com/blog/booking-com-hotel-prices-api), and for a polling loop around prices, [Build a Price Tracking Agent in Python](https://perceptlayer.com/blog/price-tracking-agent-python). Store dates as [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) and currencies as [ISO 4217](https://www.iso.org/iso-4217-currency-codes.html) codes.

## APIs in this post

-   [Booking.com **1 credit per call** · 11 endpoints](https://perceptlayer.com/apis/booking)
-   [Expedia **1 credit per call** · 4 endpoints](https://perceptlayer.com/apis/expedia)
-   [Trivago **1 credit per call** · 6 endpoints](https://perceptlayer.com/apis/trivago)

## Frequently asked questions

### How many credits does a hotel rate check cost?

Each successful call costs 1 credit on these endpoints. Checking 200 hotels over 30 check-in dates with a rooms call is 200 × 30 × 1 credit = 6,000 credits per source and occupancy. Failed calls cost nothing and return `X-Credits-Used: 0`. Credit pricing is on the [pricing page](https://perceptlayer.com/pricing).

### Can I get room-level prices, or only one price per hotel?

Both. Booking `rooms` and Expedia `hotels-rooms` return prices per room, and Expedia adds one entry per rate plan. Booking `prices`, Expedia `hotels-search` and Trivago `search` return hotel-level results. The calls are separate, so budget for each one rather than assuming a search includes rooms.

### Do the responses include cancellation terms?

Not in the room responses. Expedia's search results include a hotel-level `free_cancellation` boolean, which is the only cancellation field in the recordings. If refundable and non-refundable rates must be tracked separately, check the page for each rate plan against your results before relying on it.

### How fast can I run a 6,000-call sweep?

At 5 requests a second, the minimum for paying accounts, 6,000 calls take 20 minutes, but that is a ceiling: throughput is workers ÷ latency, so 4 workers at about 2 seconds a call manage roughly 2 a second. A free account runs at 1 request a second, so the same count would take 100 minutes, and its free credits cover only 1,000 calls. Rate-limited responses carry `Retry-After`; wait that many seconds before retrying.

hotel prices rate monitoring revenue management travel booking

## More from the blog

1.  [Web data by industry](https://perceptlayer.com/blog/category/web-data) 28 Sept 2026 Perceptlayer 8 min read
    
    ## [Flight Price Data: What Fare Tracking Can and Cannot Show](https://perceptlayer.com/blog/flight-price-data-fare-tracking)
    
    What one recorded JFK to LAX call shows: price, stops, duration and CO2 per flight. Plus what a single snapshot can't tell you, and a daily tracker's cost.
    
2.  [Web data by industry](https://perceptlayer.com/blog/category/web-data) 21 Oct 2026 Perceptlayer 8 min read
    
    ## [Job Posting Data for Labour-Market Research](https://perceptlayer.com/blog/job-posting-data-labour-market-research)
    
    Which Indeed posting fields support trend analysis, where postings mislead versus hires, and a weekly collection plan that costs 3,000 credits a month.
    
3.  [Web data by industry](https://perceptlayer.com/blog/category/web-data) 6 Oct 2026 Perceptlayer 7 min read
    
    ## [Salary Data from Job Sites: Reading Ranges Without Bias](https://perceptlayer.com/blog/salary-data-job-sites-bias)
    
    Glassdoor's salaries endpoint returns p10 to p90 total pay, a currency and a national count. How to read those ranges and the biases of self-reported pay.
