How to Track Hotel Prices on Booking.com Daily
A hotel revenue manager opening the laptop on Monday morning wants to answer one question: what are competing properties in this market charging today for next weekend, and how does that compare to my own rates? An affiliate marketer running a deal site wants the same answer at scale, across hundreds of properties, every day. Neither audience can pull this data from a partner API without weeks of approval and stripped-down fields. Both end up scraping the public Booking.com search interface, persisting nightly rates to a database, and computing the booking-curve over time. This guide walks the full daily-snapshot pipeline: building the search URL with destination + dates + guests, submitting the scrape, persisting the rates, and triggering alerts when prices drop or when a competitor undercuts.
What Daily Hotel Price Tracking Actually Means
The market-research literature calls this "rate parity monitoring" or "rate shopping," and it answers four questions that drive every pricing decision in hospitality. What is the prevailing nightly rate for properties comparable to mine on a given check-in date? How has that rate moved over the last 7, 14, and 30 days? Is a specific competitor cutting prices to fill empty rooms? And how does the rate at 60 days out compare to the same property at 30, 14, and 7 days out — the booking curve — so a manager can decide whether to hold rates or release inventory.
A single Booking.com search return is not enough. Each search is a point-in-time snapshot for one destination, one check-in window, one guest configuration. The tracking value comes from running the same query daily and persisting every result, so the time series reveals the curve.
What Booking Returns Per Property
A search call against the Booking.com /searchresults.html page returns a list of properties, and each property carries a structured payload that includes:
| Field | Example |
|---|---|
name | The Langham London |
property_id | Booking's internal numeric identifier |
url | Direct property URL for follow-up detail pulls |
address | 1c Portland Place, London W1B 1JA |
latitude / longitude | 51.518, -0.143 |
star_rating | 5 |
review_score | 9.2 |
review_count | 4,318 |
property_type | Hotel / Apartment / Hostel / Guesthouse |
nightly_price | The displayed per-night rate in the active currency |
total_price | Full-stay price including taxes and fees |
currency | GBP |
original_price | If a Genius or mobile discount is active, the strike-through rate |
discount_percent | Numeric discount surfaced by Booking |
is_sold_out | Boolean — useful as a demand signal |
room_type | "Deluxe King Room" or similar |
cancellation_policy | Free cancellation / non-refundable |
breakfast_included | Boolean |
The property-detail endpoint adds richer fields including the full room-rate matrix (every available room type and price for the selected dates), photo gallery, and complete amenities list. For a daily price-tracking pipeline, the search endpoint is usually enough — the property endpoint is reserved for deeper drill-downs on a flagged competitor.
Building the Search URL
Booking.com encodes the entire search into the URL: destination, check-in, check-out, guests, rooms, currency, and any filters. The cleanest way to build one is to perform the search interactively in a browser and copy the URL.
- Open
booking.com. - Type the destination, pick the check-in and check-out dates, set guests and rooms.
- Click Search.
- Copy the URL from the address bar.
A typical search URL looks like:
https://www.booking.com/searchresults.html?ss=London&checkin=2026-06-12&checkout=2026-06-14&group_adults=2&no_rooms=1&group_children=0
The query parameters that matter for daily tracking:
| Param | Purpose |
|---|---|
ss | Destination string (city, neighborhood, or landmark) |
checkin | Check-in date, YYYY-MM-DD |
checkout | Check-out date, YYYY-MM-DD |
group_adults | Adult guest count |
no_rooms | Number of rooms |
group_children | Child guest count |
nflt | Optional filter string for star rating, property type, neighborhood |
Three example URLs to test the pipeline with:
https://www.booking.com/searchresults.html?ss=London&checkin=2026-06-12&checkout=2026-06-14&group_adults=2&no_rooms=1
https://www.booking.com/searchresults.html?ss=Tokyo&checkin=2026-07-05&checkout=2026-07-07&group_adults=2&no_rooms=1
https://www.booking.com/searchresults.html?ss=Miami&checkin=2026-08-15&checkout=2026-08-17&group_adults=2&no_rooms=1
For the booking-curve work, the same destination is queried with multiple check-in dates — one URL per check-in. A rolling 14-date window (7, 14, 21, 30, 45, 60, 75, 90 days ahead) per destination is the standard shape.
The API Call
Every Booking.com endpoint is asynchronous: submit a job, poll for status, fetch the result when complete. Submit with curl first to confirm the URL works:
curl -G "https://api.logposervices.com/api/v1/travel/booking/search" \
-H "X-API-Key: lp_xxxxxxx" \
--data-urlencode "url=https://www.booking.com/searchresults.html?ss=London&checkin=2026-06-12&checkout=2026-06-14&group_adults=2&no_rooms=1" \
--data-urlencode "rows_per_page=25"
# → {"job_id": "bk_a4f9..."}
curl -H "X-API-Key: lp_xxxxxxx" \
"https://api.logposervices.com/api/v1/jobs/bk_a4f9?wait=true&timeout=60"
curl -H "X-API-Key: lp_xxxxxxx" \
https://api.logposervices.com/api/v1/jobs/bk_a4f9/result
The rows_per_page parameter caps how many properties come back in one call (default 25, max around 100). The offset parameter pages through the result set when more depth is needed. For most daily tracking pipelines the top 25 properties per destination are enough — those are the listings Booking surfaces to a real guest on the first page, and the only ones an affiliate marketer actually competes for.
If a deeper per-property rate breakdown is needed — for example, every room type a competitor is offering on a given night — chain a second call to /api/v1/travel/booking/property using the url from the search result:
curl -G "https://api.logposervices.com/api/v1/travel/booking/property" \
-H "X-API-Key: lp_xxxxxxx" \
--data-urlencode "url=https://www.booking.com/hotel/gb/the-langham-london.html?checkin=2026-06-12&checkout=2026-06-14&group_adults=2"
For destinations where the goal is the full picture in a single call, the combined search-and-extract endpoint pulls the search list and resolves per-property detail in one job — useful when running a small daily watchlist of competitor hotels.
The Python Pipeline
This is the script most teams run on a cron. It takes one destination URL, fires the search, and writes a snapshot row per property to a CSV (or a database — the persistence step is interchangeable).
import os, time, csv, datetime, requests
API_KEY = os.environ["LOGPOSE_API_KEY"]
BASE = "https://api.logposervices.com/api/v1"
HEADERS = {"X-API-Key": API_KEY}
def submit_and_wait(path: str, params: dict, timeout_s: int = 120) -> dict:
r = requests.get(f"{BASE}/{path}", params=params, headers=HEADERS, timeout=30)
r.raise_for_status()
job_id = r.json()["job_id"]
deadline = time.time() + timeout_s
while time.time() < deadline:
s = requests.get(f"{BASE}/jobs/{job_id}", headers=HEADERS, timeout=15).json()
if s["status"] == "completed":
break
if s["status"] == "failed":
raise RuntimeError(s.get("error", "unknown failure"))
time.sleep(2)
else:
raise TimeoutError(f"job {job_id} did not finish in {timeout_s}s")
return requests.get(f"{BASE}/jobs/{job_id}/result", headers=HEADERS, timeout=15).json()
def snapshot_destination(search_url: str, out_path: str) -> int:
data = submit_and_wait(
"travel/booking/search",
{"url": search_url, "rows_per_page": 25},
)
properties = data["properties"]
snapshot_date = datetime.date.today().isoformat()
write_header = not os.path.exists(out_path)
with open(out_path, "a", newline="", encoding="utf-8") as f:
w = csv.DictWriter(
f,
fieldnames=[
"snapshot_date", "property_id", "name", "star_rating",
"review_score", "nightly_price", "total_price", "currency",
"discount_percent", "is_sold_out", "room_type",
],
extrasaction="ignore",
)
if write_header:
w.writeheader()
for p in properties:
row = {"snapshot_date": snapshot_date, **p}
w.writerow(row)
return len(properties)
if __name__ == "__main__":
url = (
"https://www.booking.com/searchresults.html"
"?ss=London&checkin=2026-06-12&checkout=2026-06-14"
"&group_adults=2&no_rooms=1"
)
n = snapshot_destination(url, "london_2026-06-12.csv")
print(f"wrote {n} rows")
Schedule the script with cron, a workflow runner, or a serverless scheduler. The file appends per day, so after a week the CSV contains seven snapshots and the price-curve is visible by grouping on property_id and ordering by snapshot_date.
Persisting Snapshots Properly
A flat CSV works for a single destination and a short window. For a real revenue-management or affiliate workflow, persist into a database with a composite key:
CREATE TABLE booking_snapshots (
snapshot_date DATE NOT NULL,
property_id BIGINT NOT NULL,
destination TEXT NOT NULL,
checkin_date DATE NOT NULL,
checkout_date DATE NOT NULL,
name TEXT,
star_rating INT,
review_score NUMERIC(3,1),
nightly_price NUMERIC(10,2),
total_price NUMERIC(10,2),
currency CHAR(3),
discount_percent INT,
is_sold_out BOOLEAN,
room_type TEXT,
PRIMARY KEY (snapshot_date, property_id, checkin_date)
);
The (snapshot_date, property_id, checkin_date) primary key is the trick. It guarantees one row per property per check-in per day. A query that joins the latest snapshot to a 7-day-ago snapshot reveals price movements directly:
SELECT
today.name,
today.checkin_date,
today.nightly_price AS price_today,
week_ago.nightly_price AS price_week_ago,
today.nightly_price - week_ago.nightly_price AS delta
FROM booking_snapshots today
JOIN booking_snapshots week_ago
ON today.property_id = week_ago.property_id
AND today.checkin_date = week_ago.checkin_date
AND week_ago.snapshot_date = today.snapshot_date - INTERVAL '7 days'
WHERE today.snapshot_date = CURRENT_DATE
AND today.destination = 'London'
ORDER BY delta ASC
LIMIT 20;
That single query produces the daily "biggest price drops" report most revenue managers want to see at 09:00. A second query joining the latest snapshot to the 30-day-ago snapshot yields the monthly trend; a third joining to the same calendar day one year prior yields the year-over-year comparison that finance teams need for forecasting. All three reports are derivatives of the same simple persistence shape — which is why getting the schema right on day one matters more than picking the right database engine.
The other reason to use a structured store rather than a pile of CSVs is the dedupe story. Booking occasionally shows the same property under slightly different display names (the brand name vs. the booking-engine name vs. a campaign-specific alias), but the numeric property_id is stable. Keying every row on property_id rather than name guarantees that a re-branded property still belongs to the same time series, and the historical rate-curve remains continuous through ownership changes or distribution-platform updates.
Building the Booking Curve
The booking curve is the price for a given check-in date plotted against days-to-arrival. It is the single most useful artifact of a daily tracking pipeline. To build it, snapshot the same property with multiple check-in dates every day:
import datetime
DESTINATION = "London"
TARGET_DATES_AHEAD = [7, 14, 21, 30, 45, 60, 75, 90]
today = datetime.date.today()
for days in TARGET_DATES_AHEAD:
checkin = today + datetime.timedelta(days=days)
checkout = checkin + datetime.timedelta(days=2)
url = (
"https://www.booking.com/searchresults.html"
f"?ss={DESTINATION}&checkin={checkin}&checkout={checkout}"
"&group_adults=2&no_rooms=1"
)
snapshot_destination(url, f"{DESTINATION.lower()}_curve.csv")
After two weeks of daily runs, the database holds enough rows to plot the curve per property: x-axis days-to-arrival, y-axis nightly rate. A property whose curve climbs steeply in the final 14 days is high-demand and inventory-constrained. A property whose curve drops in the final week is over-supplied and dumping rooms — that is the property a deal-site affiliate flags for a featured listing.
There are three booking-curve shapes that recur across markets, and recognizing them is what separates a useful rate-intelligence pipeline from a wall of numbers. The first is the steady climb — prices increase monotonically as the check-in date approaches, usually 10–30% from 60 days out to the week of arrival. This is the curve for a property in a healthy demand market with disciplined revenue management. The second is the late dump — prices hold flat for weeks, then collapse 15–40% in the final 5 to 10 days as the property realizes it will not sell out. This is the curve for an over-built market or a property with weak distribution. The third is the spike-and-fade — prices jump 50%+ around a specific event date (a conference, a festival, a sports event), then revert to baseline immediately on either side. Detecting which shape applies to a given destination-week tells a revenue manager whether to hold rates, drop them, or open up an event surcharge.
The booking-curve dataset is also the input to compset benchmarking: pick a set of comparable hotels (same star rating, similar location, similar amenities) and compute the relative position of each property on each snapshot date. A property that is consistently +5% above its compset is a price leader; a property that is consistently -8% below is leaving money on the table. The daily snapshot makes this benchmarking continuous instead of the quarterly STR-report exercise it used to be.
Alerting on Price Drops
For revenue managers, the actionable signal is not the daily snapshot itself — it is the alert that fires when a tracked competitor undercuts the room rate. The simplest implementation runs after each snapshot and compares to a rolling baseline.
import statistics
WATCHED_COMPETITORS = [12345, 67890, 13579] # property_id values
def check_for_drops(conn, destination: str, checkin: str):
cur = conn.cursor()
cur.execute(
"""
SELECT property_id, name, snapshot_date, nightly_price
FROM booking_snapshots
WHERE destination = %s
AND checkin_date = %s
AND property_id = ANY(%s)
AND snapshot_date >= CURRENT_DATE - INTERVAL '14 days'
ORDER BY property_id, snapshot_date
""",
(destination, checkin, WATCHED_COMPETITORS),
)
by_property = {}
for pid, name, sd, price in cur.fetchall():
by_property.setdefault(pid, {"name": name, "rows": []})["rows"].append((sd, price))
alerts = []
for pid, info in by_property.items():
rows = sorted(info["rows"])
if len(rows) < 8:
continue
latest_price = float(rows[-1][1])
baseline = statistics.median(float(p) for _, p in rows[-8:-1])
if latest_price < baseline * 0.90:
alerts.append({
"property_id": pid,
"name": info["name"],
"latest": latest_price,
"baseline": baseline,
"drop_pct": round((1 - latest_price / baseline) * 100, 1),
})
return alerts
A 10% drop against the rolling 7-day median, sustained across two snapshots, is the threshold most revenue teams converge on after a few months of tuning. Lower thresholds produce noise; higher thresholds miss the early movers. Route the alert payload to Slack, an email digest, or directly into a revenue-management dashboard.
Pinning a Geography
Booking serves different prices to different IP geographies — US visitors see USD with a US-resident discount, Japanese visitors see JPY with no discount, mobile user-agents see mobile-only deals. For a price-tracking pipeline this means the snapshot is only comparable to itself if the geography is held constant.
The fix is to pin the proxy region on every call so every snapshot in the time series is fetched from the same vantage point. Pick the geography that matches the buying audience for the use case — a UK affiliate site should snapshot from a UK IP, a corporate-travel team in Singapore should snapshot from a Singapore IP — and never mix vantage points within the same (property_id, checkin_date) time series. If multi-market comparison is the goal, run parallel pipelines (one per vantage point) and label every row with the geography it was fetched from.
Scaling Beyond One Destination
A serious rate-intelligence pipeline tracks dozens of destinations and hundreds of check-in dates per day. The math is destinations × checkin_dates × 1 call, which for a 10-destination, 14-checkin-date watchlist is 140 calls per day. Sequential execution at 60 seconds per job is just under 2.5 hours of wall-clock per cycle — fine for a nightly run, painful if the cron-window is tight.
The LogPose Booking endpoint exposes a bulk submission shape that runs multiple targets in parallel up to the per-user concurrency cap:
requests.post(
"https://api.logposervices.com/api/v1/travel/booking/search/bulk",
headers={"X-API-Key": os.environ["LOGPOSE_API_KEY"]},
json={
"targets": [
{"url": "https://www.booking.com/searchresults.html?ss=London&checkin=2026-06-12&checkout=2026-06-14&group_adults=2&no_rooms=1"},
{"url": "https://www.booking.com/searchresults.html?ss=London&checkin=2026-06-19&checkout=2026-06-21&group_adults=2&no_rooms=1"},
{"url": "https://www.booking.com/searchresults.html?ss=London&checkin=2026-06-26&checkout=2026-06-28&group_adults=2&no_rooms=1"},
],
},
).raise_for_status()
A 140-target bulk submission typically finishes in 10 to 20 minutes wall-clock instead of 2+ hours sequential. The same pattern works for the property-detail endpoint when a watchlist of specific competitor hotels is being drilled into for full room-rate matrices.
Legality and Ethics
Booking.com nightly rates are public commercial data, displayed to any anonymous visitor with the right URL. Scraping them for competitive price intelligence is on settled legal ground in the US (CFAA does not apply to public data per hiQ Labs v. LinkedIn) and is broadly compliant under EU competition-law guidance, which explicitly recognizes price monitoring as a legitimate competitive-intelligence activity. The terms-of-service constraints that apply to Booking's partner XML feeds and the affiliate Demand API do not extend to anonymous browsing of the public search interface. The downstream-use question — using the data to inform internal pricing decisions, feed an affiliate site's deal listings, or build a hotel-rate research product — is where the real legal review should happen, not on the scrape step itself.
Common Mistakes
- Snapshotting at irregular times. Booking rates update a handful of times per day at provider-specific intervals; running the snapshot at the same wall-clock time daily (typically pre-dawn in the destination's timezone) produces clean time series. Random firing times introduce noise that masks real movements.
- Mixing IP geographies in one time series. A US-IP snapshot on Monday and an EU-IP snapshot on Tuesday will look like a 15% price change that is actually currency conversion plus a Genius-discount difference. Pin the geography.
- Treating sold-out as missing data. When
is_sold_out=true, persist the row with that flag rather than dropping it. The transition from available-to-sold-out is itself one of the strongest demand signals in the dataset. - Polling individual properties multiple times per day. The data does not change that often. One daily snapshot per property per check-in date is the right cadence; anything more is wasted proxy budget.
- Ignoring the Cloudflare 100-second edge timeout.
api.logposervices.comsits behind Cloudflare, so any single job that takes 100+ seconds returns a 524 to the client even though the work continues server-side. Always poll for status via/api/v1/jobs/{job_id}; never expect a synchronous response on long-running jobs.
Scaling With LogPose
The LogPose Booking.com endpoints (/api/v1/travel/booking/search, /api/v1/travel/booking/property, /api/v1/travel/booking/reviews, /api/v1/travel/booking/search-and-extract) handle the proxy rotation, anti-bot evasion, and parsing that make a daily snapshot pipeline reliable across thousands of properties without per-call hand-tuning. The bulk endpoints (/booking/search/bulk, /booking/property/bulk) fan a destination watchlist out across the proxy pool in parallel, which is the difference between a 2-hour nightly cron and a 15-minute one. For revenue managers and affiliate teams running multi-destination, multi-date pipelines, the managed shape removes the operational tax of maintaining a scraper in-house.
Get Started
- Sign up at logposervices.com and generate an API key under Tool → API Keys.
export LOGPOSE_API_KEY=lp_xxxxxxx- Build a Booking.com search URL for one destination + check-in date in the browser, then run the Python script above against it.
- Schedule the script daily at the same wall-clock time, with results appending to a database keyed on
(snapshot_date, property_id, checkin_date).
Related reading: How to build a competitor price-monitoring pipeline for the cross-platform pattern behind any daily-snapshot system, and the web scraping API guide for the broader DIY-vs-managed comparison when standing up a new rate-intelligence pipeline.
External: Booking.com, hiQ Labs v. LinkedIn.