How to scrape Booking.com hotel prices for exact dates
Booking.com prices are readable without a browser, but two of its behaviours will make your numbers wrong if you do not design around them: it prices a stay per session, and its default ordering is personalised and reshuffles between pages.
The short version
- Ask for a stay, not a page. Check-in, nights, adults, children's ages, rooms and currency define the price. A solo traveller and a couple see different rates for the same room.
- Use a deterministic sort. The default "top picks" order is personalised and reshuffles between pages, so a long paged run repeats properties and misses others. Sort by price, review score, stars or distance for a repeatable list.
- Filter before billing. A price ceiling and a star minimum both run before the charge, and the star filter runs on Booking's side.
- Compare medians, not single reads. Because a stay is priced per session, two runs seconds apart can differ. Give alerts a threshold and take a median for anything that has to be defensible.
Per-session pricing is the fact that changes your design
Run the same search twice, seconds apart, and Booking can quote you different totals. The variation runs from a few per cent to about a quarter. It is not a parsing error and it is not your proxy: the site rotates promotional rates, mobile-only deals and currency handling per session, and the struck-through pre-discount price is present in some answers and absent in others.
Three consequences, and they are the difference between a price feed you can act on and one that cries wolf:
- Give every alert a threshold. A 3% drop is noise. Set the bar where a human would care, and the mail stays readable.
- Take a median for anything defensible. If a number is going into a rate-parity report or a board deck, sample the stay more than once and use the median.
- Compare like with like. Same party, same currency, same lead time, same sort. Rows carry check-in, nights, adults, rooms and currency for exactly this reason — rows from different days line up in one table with no post-processing.
The ordering trap
Booking's default order is "top picks", which is personalised and reshuffles between page requests. Page a destination under it and you will see the same property twice and miss others entirely — a deduplicated run reports the repeats, but the misses are invisible.
For any run that pages, sort by price, review score, stars or distance. Those orders are stable, which makes the walk complete and the run reproducible tomorrow.
Working code
from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("kestrel/booking-prices-scraper").call(run_input={
"locationQueries": ["Lisbon"],
"checkIn": "30 days", "nights": 2, "adults": 2, "rooms": 1,
"currency": "EUR",
"sortBy": "price", # deterministic, unlike "top picks"
"maxHotelsPerQuery": 100,
"maxPrice": 150, # filters before billing
"minStars": 4, # Booking's own star filter
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
if row["type"] == "hotel" and not row["sold_out"]:
print(row["name"], row["price_total"], row["currency"],
"taxes included" if row["taxes_included"] else f'+{row["taxes_fees_excluded"]} taxes',
"| free cancellation" if row["free_cancellation"] else "")
For a comp set you re-price every morning, use numeric property ids — no lookup, no autocomplete, no ambiguity:
curl -X POST "https://api.apify.com/v2/acts/kestrel~booking-prices-scraper/run-sync-get-dataset-items?token=<YOUR_APIFY_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"hotelIds": ["536251","2508912"], "checkIn": "45 days", "nights": 2, "adults": 2, "currency": "USD"}'
What a row carries
One hotel row per property: the lowest stay total Booking shows for your party, the nightly rate derived from it, the total in the hotel's own currency, what is excluded and Booking's wording for it, free-cancellation, no-prepayment and breakfast flags, stars, the 1-10 review score and count, address, coordinates, neighbourhood and distance from the centre, the room behind the headline price, and — when the property cannot be booked — the sold-out flag with the alternative dates Booking offers.
Optional room rows add every room-and-rate combination Booking matched to the stay, each with its own price, occupancy, meal plan, free-cancellation deadline and the "only 2 left" urgency message. A room can legitimately appear twice with different meal plans or cancellation policies; those are different rates, not duplicates.
One number worth understanding: the nightly price is the stay total divided by nights, not Booking's own per-night figure, because Booking omits a per-night number on most searches. Dividing is the honest reconstruction, and it is stated rather than hidden.
Destination, comp set or portfolio
- Destinations are resolved through Booking's own autocomplete, so a city, region, district, landmark, airport or country all work. A destination that happens to name a single property prices just that property, and the status row says so rather than silently returning one row for a city.
- Property URLs carry no numeric id, so they are resolved through autocomplete first and held to the country in the URL.
- Names and numeric ids are the fastest path, and ids need no lookup at all.
A property that turns up twice — in two destinations, or as a URL and its id — is delivered and billed once.
How this compares with Google Hotels
Booking gives you Booking's price, with room-level detail, cancellation policies and the tax wording. Google Hotels gives you every source's price for the same property in one row, including the hotel's own site, but at occupancy level rather than room level.
For rate shopping across channels, start with Google Hotels. For depth on one channel — which room, which meal plan, which cancellation deadline — read Booking directly. For parity work, the rate parity checker does the comparison for you.
The honest limitations
- Residential proxy required. Datacenter gets the WAF challenge on the search call.
- Prices are for your party, and change with adults, children's ages and rooms.
- Taxes vary by market, with flags per row saying which.
- The default order reshuffles — use a deterministic sort for paged runs.
- Per-session pricing is the site's behaviour, not something a scraper can normalise away.
Run it
Booking.com Prices Scraper — $0.004 per priced property and $0.002 per room row, with sold-out properties, status rows, unknown ids and filtered rows free. Related: Booking.com reviews without a browser and tracking competitor prices daily with n8n.
FAQ
Why do two runs minutes apart return different prices?
Because Booking.com prices a stay per session. Differences of a few per cent up to about a quarter are normal: the site rotates promotional rates, mobile-only deals and currency handling per session, and the pre-discount price is present on some answers and absent on others. It is the site's behaviour, not a parsing artefact — every scraper of Booking sees it.
Do I need a residential proxy?
For prices, yes. Booking's search call answers datacenter addresses with its WAF challenge and serves residential ones without cookies or tokens. Reviews are different — that endpoint is not walled, which is why review rows are cheaper than price rows.
Are taxes included in the total?
It depends on the market. In some countries the total includes taxes; elsewhere Booking lists them as excluded. Every row carries flags for whether taxes are included, the excluded amount, and Booking's own wording for the charges, so you never have to assume.
What happens to sold-out properties?
They are still delivered, free, flagged as sold out and carrying the alternative dates Booking suggests. With a price ceiling set they are dropped instead, since they have no price to compare.
Run Booking.com Scraper on Apify →
Other actors used on this page: apify.com/kestrel/booking-reviews-scraper, apify.com/kestrel/google-hotels-prices.