A small hotel sets its rates by looking at the neighbours. Somebody opens Booking.com, types the city and the dates of the next busy weekend, scrolls through the hotels that look comparable and writes a few prices down. It works once; it does not work every morning for six different dates. This tutorial builds a Booking.com price tracker in Python: the same search, run every day, each hotel compared with the previous run, and the results collected into a competitor rate grid with pandas. The data comes from the Booking.com price tracker mode of the Booking.com Hotels Scraper, an Apify Actor called through the official Python client.
What you need
- An Apify account (the free plan is enough to try this).
- Your API token, from Apify Console → Settings → API & Integrations, exported as an environment variable:
export APIFY_TOKEN=... - Python 3 and two packages:
pip install "apify-client>=3,<4" pandas
What the tracker compares
The Actor reads Booking.com search results for a destination and a stay and returns one row per property: total price of the stay (priceTotal), the taxes and fees Booking.com shows as excluded (taxesAndFees), the strike-through price of a deal (priceBeforeDiscount), review score on 10, number of reviews, stars, type, neighbourhood, coordinates and a link that opens the property for your dates.
With monitorPrices on, it also keeps the total price of every property it compared in a key-value store of your Apify account (booking-price-monitor by default, set by monitorStoreName), with one record per search. A search is the destination, the stay, the guests, the rooms, the sort order, the filters, the currency and the language. Every later run of the same search fills four columns: changeType (new, price_changed or unchanged), previousPrice, priceDelta and priceDeltaPercent. The reference lives in your account, so a script started by cron and a schedule in Apify Console behave the same way.
Step 1: the input
This is the public task "Monitor Booking.com hotel prices in Lisbon daily", with its relative dates replaced by a fixed stay, a Friday-to-Sunday weekend in December:
run_input = {
"location": "Lisbon, Portugal",
"destId": -2167973,
"filters": "ht_id=204",
"checkIn": "2026-12-04",
"checkOut": "2026-12-06",
"adults": 2,
"currency": "EUR",
"maxResults": 60,
"monitorPrices": True,
}
The fields not shown keep the Actor's defaults (one room, English, Booking.com's top-picks order). destId is the dest_id of a Booking.com search URL, the reliable way to name a destination; filters is Booking.com's own filter string, and ht_id=204 keeps hotels only. With monitoring on, maxResults is the number of properties compared by each run.
To get closer to your actual competitive set, narrow the search with starRatings, minReviewScore, priceMin / priceMax per night or freeCancellation. Those filters are recent: check the stars and scores of the first rows to make sure they apply as you expect.
Without onlyChanges, every compared hotel comes back every day with its label, unchanged included: a full daily snapshot of your market, which is what a rate grid needs. Add "onlyChanges": True for an alert version that saves and charges only the new hotels and the price changes.
Step 2: the daily script
Save this as track_booking.py. It runs the search, prints the moves and appends the snapshot to a CSV file:
import os
import pandas as pd
from apify_client import ApifyClient
HISTORY = "booking-lisbon-dec.csv"
run_input = {
"location": "Lisbon, Portugal",
"destId": -2167973,
"filters": "ht_id=204",
"checkIn": "2026-12-04",
"checkOut": "2026-12-06",
"adults": 2,
"currency": "EUR",
"maxResults": 60,
"monitorPrices": True,
}
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("myth84/booking-hotels-scraper").call(run_input=run_input)
rows = list(client.dataset(run.default_dataset_id).iterate_items())
for item in rows:
if item["changeType"] == "price_changed":
print(f'{item["title"]}: {item.get("previousPrice")} -> {item["priceTotal"]} '
f'{item["currency"]} ({item.get("priceDeltaPercent")} %)')
df = pd.DataFrame(rows)
df.to_csv(HISTORY, mode="a", header=not os.path.exists(HISTORY), index=False)
print(f"{len(df)} hotels saved to {HISTORY}")
The first run is the reference: every hotel comes back as new. A run that finds nothing fails with an explicit message in its log (destination not recognised, dates in the past, filters too strict), so a broken schedule does not look like a calm market.
The compared price is priceTotal: the total of the stay after discounts and before the charges Booking.com lists as excluded. A change in the excluded charges alone is not a price change for the tracker.
Step 3: the competitor rate grid
After a few days, turn the history into one row per hotel and one column per day:
import pandas as pd
history = pd.read_csv("booking-lisbon-dec.csv")
history["day"] = history["scrapedAt"].str[:10]
history["allIn"] = history["priceTotal"] + history["taxesAndFees"].fillna(0)
history["allInPerNight"] = history["allIn"] / history["nights"]
grid = history.pivot_table(index="title", columns="day", values="allInPerNight", aggfunc="last")
print(grid.round(0))
print("Market median per night:", grid.median().round(0).to_dict())
deals = history[(history["changeType"] == "price_changed") & history["priceBeforeDiscount"].notna()]
print(deals[["day", "title", "previousPrice", "priceTotal", "badges"]])
priceTotal + taxesAndFees is what a guest actually pays on Booking.com; divided by nights, it gives an all-in nightly rate to compare with your own. The median of each column is your market rate for that weekend, day after day. A drop that comes with priceBeforeDiscount (and often a "Limited-time Deal" badge) is a promotion; a drop without them is a change of the base rate.
The tracker watches a search, so it returns more hotels than you compete with. Filter on the platformListingId of the hotels you care about, or on region (the neighbourhood), starRating and rating. Over a few weeks, the grid shows how far ahead your neighbours start moving for a busy weekend, whether they cut or raise as it gets closer, and which of them follow each other.
Step 4: schedule it
With cron, one line runs the script at 7:00 every day (make sure APIFY_TOKEN is set in the cron environment):
0 7 * * * cd /path/to/tracker && python3 track_booking.py >> tracker.log 2>&1
Without code: open the public task in Apify Console, set your dates, save it as your own task and add a daily schedule; download the dataset as CSV whenever you want. One task or script per date you follow: several can share the same monitorStoreName, since each search has its own record. Stop the schedule after check-in, since a search for dates in the past fails.
The original task uses relative dates instead (checkInDaysFromNow 30, nights 3): the record then follows the price at a constant lead time, and a change mixes the hotel's pricing with the calendar. Useful for a general view of your market; just know which of the two you are tracking.
Honest limits
-
Rankings move. The top-picks order varies between sessions, so the compared set drifts a little from run to run. A property that drops out and comes back is not reported as new: its last price is kept for 90 days. Sorting by
pricegives a steadier set, but a hotel that becomes more expensive can then leave the firstmaxResults. - No sold-out signal. A hotel that sells out for your dates simply stops appearing. In the grid it shows as a gap, which can also mean it was ranked lower that day.
- One price per property. The row carries the price of the search card for your guests and rooms, not every room type and rate plan.
-
Same currency only. If Booking.com answers in another currency, the property is reported
unchangedand the new currency becomes the reference. - A bad day never erases the reference. A failed or interrupted run does not overwrite the record.
- No personal data. Property facts, the average score and the number of reviews; nothing about guests or reviewers.
Pricing is pay-per-event: a fixed price per property saved (see the Pricing tab of the Actor). Use the data for analysis, respect the platform's terms and local law, and do not republish listings.
Disclosure: I build these Actors and may earn a commission if you sign up to Apify through these links.
Top comments (0)