kestrel actors docs

kestrel actorsGuides

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

  1. 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.
  2. 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.
  3. Filter before billing. A price ceiling and a star minimum both run before the charge, and the star filter runs on Booking's side.
  4. 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:

  1. Give every alert a threshold. A 3% drop is noise. Set the bar where a human would care, and the mail stays readable.
  2. 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.
  3. 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

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

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.

More guides