๐ŸŽ‰ Premium Proxies ยท 3-Day Free TrialClaim Now โ†’
Guides

How to Scrape Booking.com Hotel Prices

Learn how to scrape Booking.com hotel prices ethically: handle dynamic JS pricing, currency and geo, and aggressive anti-bot with a Scraping API and code.

S SparkProxy 22 18 min read
Share
How to Scrape Booking.com Hotel Prices

To scrape Booking.com hotel prices reliably you have to solve four problems at once: prices that hydrate through JavaScript, a URL that only returns a price when the dates and occupancy are correct, a currency and geo layer that quietly changes the number you see, and one of the more aggressive anti-bot stacks on the travel web. Skip any one of them and you get an empty page, a stale price, or a challenge screen. This guide walks through each layer with working code against the SparkProxy Scraping API, and keeps the whole thing on the ethical side of the line: public listing data only, robots and Terms of Service respected, no personal data, polite request rates.

Key takeaways

  • Booking.com prices are date, occupancy, and currency dependent. You must set checkin, checkout, group_adults, and selected_currency in the URL or the price is meaningless.
  • The price and lazy-loaded result cards hydrate with JavaScript, so render_js=true is required for a complete page.
  • Target data-testid attributes, not CSS class names. Booking.com rotates obfuscated classes but keeps test IDs stable across deploys.
  • Match country_code to the region of the currency you request, or geo logic will shift the displayed rate.
  • Collect only public, non-personal listing data, throttle politely, and respect robots.txt and the site's Terms of Service.

What data can you scrape from a Booking.com listing?

Every property card on a search results page carries the same core fields. These are the ones worth targeting for hotel price scraping, and the stable selector you should anchor to.

FieldExample valueAnchor selector (starting point)Notes
Hotel name`Hotel de Crillon``[data-testid="title"]`One per property card.
Nightly / total price`$412``[data-testid="price-and-discounted-price"]`Reflects your dates, occupancy, and currency.
Review score`9.2``[data-testid="review-score"]`Out of 10; absent for unrated properties.
Review count`1,284 reviews`inside `[data-testid="review-score"]`Sits next to the score.
Room type`Deluxe Double Room``[data-testid="recommended-units"]`The unit shown for your search dates.
Availability dates`2026-08-01 to 2026-08-03`from your request, not the DOMThe dates you passed drive which rate returns.
Star rating`5 stars``[data-testid="rating-stars"]`Property class, distinct from review score.
Location`8th arr., Paris``[data-testid="address"]`District plus distance from center.

The date range is the one field you provide rather than extract. Booking.com prices are a function of the exact checkin and checkout you request, so record the dates alongside every price you store. A price with no date attached is noise.

Free trial

Scraping at scale? Skip the blocks.

Fast, unblockable datacentre proxies with unlimited bandwidth.

How Booking.com search URLs work

The search results page is a plain GET request, and every parameter that shapes the result lives in the query string. That is good news: you can build any search by assembling a URL, no form submission or clicking required.

ParameterExampleWhat it controls
`ss``Paris`The destination search string.
`dest_id``-1456928`Booking's internal destination ID (more precise than `ss`).
`dest_type``city`Type of the destination: `city`, `region`, `hotel`, `airport`.
`checkin``2026-08-01`Check-in date, ISO `YYYY-MM-DD`.
`checkout``2026-08-03`Check-out date, ISO `YYYY-MM-DD`.
`group_adults``2`Number of adults. Drives room recommendations and price.
`group_children``0`Number of children.
`no_rooms``1`Rooms requested.
`selected_currency``USD`Currency the prices are shown in.
`lang``en-us`Interface language.
`offset``25`Pagination offset. Results come 25 per page.

A complete, buildable search URL looks like this:

https://www.booking.com/searchresults.html?ss=Paris&checkin=2026-08-01&checkout=2026-08-03&group_adults=2&no_rooms=1&group_children=0&selected_currency=USD&lang=en-us

Build it in code so you can loop over dates and destinations:

from urllib.parse import urlencode

def booking_search_url(destination, checkin, checkout, adults=2, currency="USD"):
    params = {
        "ss": destination,
        "checkin": checkin,
        "checkout": checkout,
        "group_adults": adults,
        "group_children": 0,
        "no_rooms": 1,
        "selected_currency": currency,
        "lang": "en-us",
    }
    return "https://www.booking.com/searchresults.html?" + urlencode(params)

print(booking_search_url("Paris", "2026-08-01", "2026-08-03"))

The dest_id plus dest_type pair is worth capturing once per destination. It disambiguates cities that share a name (there are more than a dozen places called Paris) and returns a tighter result set than the free-text ss string alone.

Why Booking.com is hard to scrape

Three things trip up a naive scraper.

Prices hydrate with JavaScript. The initial HTML ships the page shell and some server-rendered cards, but the availability calendar and the final per-night price fill in client-side after the page loads. A plain requests.get() returns markup with placeholder prices or none at all. You need a real browser to run the page's JavaScript before you read the price.

Result cards lazy-load on scroll. Only the first several properties render on load. The rest appear as the page scrolls, which means a headless fetch that grabs the DOM at load time misses most of the list. You have to trigger scrolling to pull the full set.

Anti-bot is aggressive. Booking.com runs behavioral fingerprinting, TLS and HTTP/2 fingerprint checks, and rate-based blocking, and it serves interstitial challenge pages when a request pattern looks automated. A datacenter IP making rapid identical requests gets flagged fast. Travel and finance sites are among the strictest categories on the web for exactly this reason: the data has direct commercial value, so it is worth defending.

The practical consequence is that you need three capabilities together: JavaScript rendering, auto-scroll, and a clean residential IP that matches the geography of your query. Wiring all three by hand with Playwright plus a proxy pool works, but it is a lot of moving parts to maintain. A managed Scraping API folds them into request parameters, which is the approach below. If you are weighing the trade-off, this breakdown of a web scraping API versus self-managed proxies covers where each one wins.

Scrape Booking.com with the SparkProxy Scraping API

The SparkProxy Scraping API takes a target URL and a set of parameters, runs the page in a headless Chromium instance behind a proxy, and returns the rendered HTML. The base endpoint is https://scrape.sparkproxy.io/api/v1, and you authenticate with your key in the X-API-Key header. Sign up at app.sparkproxy.io for 1,000 free credits to test with.

Start with the minimum viable request: render the JavaScript and wait for the price element to appear before capturing.

import requests

API_KEY = "sk-your-key-here"
target = booking_search_url("Paris", "2026-08-01", "2026-08-03")

resp = requests.get(
    "https://scrape.sparkproxy.io/api/v1",
    headers={"X-API-Key": API_KEY},
    params={
        "url": target,
        "render_js": "true",                       # run the page's JavaScript
        "wait_for": '[data-testid="property-card"]',  # wait until cards exist
        "scroll": "true",                          # trigger lazy-loaded cards
    },
    timeout=120,
)
print(resp.status_code, len(resp.text))

render_js=true costs 5 credits and gives you a fully hydrated page. wait_for holds the capture until at least one property card is in the DOM, so you never read the page before the prices land. scroll=true is on by default and pulls in the lazy-loaded results.

For a target as defended as Booking.com, add a premium residential IP and pin the country. This is the request you will actually run in production:

resp = requests.get(
    "https://scrape.sparkproxy.io/api/v1",
    headers={"X-API-Key": API_KEY},
    params={
        "url": target,
        "render_js": "true",
        "premium_proxy": "true",   # residential IP, much harder to block
        "country_code": "US",      # exit in the US to match USD pricing
        "wait_for": '[data-testid="price-and-discounted-price"]',
        "stealth": "true",         # extra fingerprint hardening for tough sites
    },
    timeout=120,
)

premium_proxy=true routes through the residential pool, which real ISP addresses make far harder to detect than datacenter ranges. With render_js, that request costs 25 credits, and country_code adds 5 more, so budget 30 credits per fully rendered, geo-pinned, residential request. stealth=true adds a homepage pre-warm, a forced Google referrer, and longer idle delays, which the docs recommend specifically for travel and e-commerce targets.

If a cookie or consent banner covers the results, dismiss it before capture with a small js_scenario:

import json

resp = requests.get(
    "https://scrape.sparkproxy.io/api/v1",
    headers={"X-API-Key": API_KEY},
    params={
        "url": target,
        "render_js": "true",
        "premium_proxy": "true",
        "country_code": "US",
        "js_scenario": json.dumps({
            "instructions": [
                {"wait_for": '[data-testid="property-card"]'},
                {"click": '#onetrust-accept-btn-handler'},
                {"wait": 800},
            ]
        }),
    },
    timeout=120,
)

Extract the hotel fields

You have two clean options for pulling the fields out. Let the API do it with extract_rules, or parse the returned HTML yourself.

The extract_rules parameter takes a JSON object where each key becomes a field in the response and each value is a CSS selector. It runs server-side against the rendered DOM, so you get structured data back with no parser to maintain:

rules = {
    "hotel_name": '[data-testid="title"]',
    "price": '[data-testid="price-and-discounted-price"]',
    "review_score": '[data-testid="review-score"]',
    "room_type": '[data-testid="recommended-units"]',
}

resp = requests.get(
    "https://scrape.sparkproxy.io/api/v1",
    headers={"X-API-Key": API_KEY},
    params={
        "url": target,
        "render_js": "true",
        "premium_proxy": "true",
        "country_code": "US",
        "wait_for": '[data-testid="property-card"]',
        "extract_rules": json.dumps(rules),
        "format": "json",
    },
    timeout=120,
)
data = resp.json()

For a value string, extract_rules returns the element's text content. To pull every card into an array instead of a single match, pass the object form with a selector and a type for richer extraction, as documented in the parameter reference.

When you want full control, request the HTML and parse with BeautifulSoup. Anchor on data-testid, iterate the cards, and pair each price with the dates you searched:

from bs4 import BeautifulSoup

def parse_cards(html, checkin, checkout, currency):
    soup = BeautifulSoup(html, "html.parser")
    hotels = []
    for card in soup.select('[data-testid="property-card"]'):
        name = card.select_one('[data-testid="title"]')
        price = card.select_one('[data-testid="price-and-discounted-price"]')
        score = card.select_one('[data-testid="review-score"]')
        hotels.append({
            "name": name.get_text(strip=True) if name else None,
            "price": price.get_text(strip=True) if price else None,
            "review_score": score.get_text(" ", strip=True) if score else None,
            "checkin": checkin,
            "checkout": checkout,
            "currency": currency,
        })
    return hotels

rows = parse_cards(resp.text, "2026-08-01", "2026-08-03", "USD")

Here is the insight most tutorials miss: anchor on data-testid, never on class names. Booking.com ships obfuscated, build-hashed CSS classes like a78ca197d0 that change on nearly every deploy, so a scraper keyed to classes breaks within days. The data-testid attributes exist for their own automated tests and stay stable across deploys, which makes them the durable extraction target. When a selector does eventually change, data-testid changes far less often and more predictably.

Handle currency and geo-targeting

This is where hotel price scraping quietly goes wrong. The number Booking.com shows is not a fixed property of the listing. It depends on the currency you request and the geography of the IP that requests it. Two things must agree.

Set the currency in the URL. selected_currency=USD forces prices into dollars. Without it, Booking.com infers a currency from the visitor's location, and your dataset ends up a mix of USD, EUR, and GBP with no column telling you which is which.

Match the exit country to the currency. Request USD prices from a US IP, EUR prices from an EU IP. Geo logic can adjust displayed rates and taxes based on where the request originates, and a mismatch (asking for USD from a German IP) can return inconsistent or partially localized pricing. Pin the exit with country_code:

def scrape_market(destination, checkin, checkout, currency, country):
    url = booking_search_url(destination, checkin, checkout, currency=currency)
    return requests.get(
        "https://scrape.sparkproxy.io/api/v1",
        headers={"X-API-Key": API_KEY},
        params={
            "url": url,
            "render_js": "true",
            "premium_proxy": "true",
            "country_code": country,   # keep IP geography aligned with currency
            "wait_for": '[data-testid="property-card"]',
        },
        timeout=120,
    )

us = scrape_market("New York", "2026-09-10", "2026-09-12", "USD", "US")
de = scrape_market("Berlin",   "2026-09-10", "2026-09-12", "EUR", "DE")

country_code takes any ISO 3166-1 alpha-2 code (US, GB, DE, JP) and adds 5 credits. If you are tracking the same hotel across markets to study price discrimination or currency spread, run one request per (currency, country) pair and store the country alongside the price. This geo-consistent approach is the same foundation behind datacenter proxies for price comparison websites, where a mismatch between IP location and requested market silently corrupts the comparison.

Paginate and scale without getting blocked

Booking.com returns 25 properties per page. Walk deeper into the results by incrementing the offset parameter in steps of 25:

import time

def scrape_all_pages(destination, checkin, checkout, currency="USD", country="US", max_pages=5):
    all_hotels = []
    for page in range(max_pages):
        url = booking_search_url(destination, checkin, checkout, currency=currency)
        url += f"&offset={page * 25}"
        resp = requests.get(
            "https://scrape.sparkproxy.io/api/v1",
            headers={"X-API-Key": API_KEY},
            params={
                "url": url,
                "render_js": "true",
                "premium_proxy": "true",
                "country_code": country,
                "wait_for": '[data-testid="property-card"]',
            },
            timeout=120,
        )
        if resp.status_code != 200:
            break
        page_rows = parse_cards(resp.text, checkin, checkout, currency)
        if not page_rows:
            break
        all_hotels.extend(page_rows)
        time.sleep(2)   # polite spacing between pages
    return all_hotels

A few habits keep this sustainable and considerate of the target:

  • Space your requests. A short sleep between pages, plus the API's own delays, keeps your footprint light. There is no prize for finishing a crawl in ten seconds.
  • Rotate identity per request. The premium pool hands you a fresh residential IP each call, so no single address accumulates a suspicious volume. If you manage your own pool instead, the same discipline applies. Our guide on how to avoid getting your proxy blocked covers rotation, header hygiene, and challenge handling in depth.
  • Retry with backoff, not brute force. On a 429 or a challenge page, wait and rotate rather than retrying the same IP immediately. Hammering a block turns a temporary rate limit into a durable one.
  • Cache what you already have. Prices for fixed dates do not change minute to minute. Re-scrape on a schedule that matches how fast the data actually moves, not as fast as your code can loop.

For a broader look at the infrastructure side of large travel data scraping jobs, see using datacenter proxies for web scraping. Datacenter IPs handle high-volume, lower-defense targets cheaply; reserve the residential pool for the pages, like Booking.com search, that actively fight back.

Frequently asked questions

FAQ

Scraping public, non-personal data is broadly permitted, and US courts have held that accessing publicly available web data does not by itself violate the CFAA. Stay on the safe side by collecting only public listing and price data, avoiding anything behind a login, honoring robots.txt and the site's Terms of Service, and throttling your requests. For customer-facing products, consult legal counsel or use Booking.com's official partner API.

Because the per-night price and the availability calendar hydrate through JavaScript after the initial HTML loads, and additional result cards lazy-load as the page scrolls. A plain HTTP fetch returns placeholder or missing prices and only the first few properties. Setting render_js=true runs a real headless browser so the prices populate and scroll=true pulls in the rest of the list.

Set selected_currency in the search URL and match the Scraping API country_code to that currency's region, for example USD prices from a US exit and EUR prices from a German exit. Booking.com derives displayed rates partly from visitor geography, so a mismatch returns inconsistent numbers. Always store the currency and exit country next to each price.

Use residential IPs via premium_proxy=true, rotate the IP on every request, add stealth=true, space requests a couple of seconds apart, and retry with backoff instead of pounding a blocked address. Booking.com fingerprints behavior and TLS, so a slow, varied, residential request pattern survives where a fast datacenter loop gets challenged.

For a handful of test requests, sometimes. At any real volume, no. A single IP making repeated searches gets rate-limited and then served challenge pages quickly, and datacenter IPs are flagged faster than residential ones. Serious hotel price scraping needs rotating IPs, which is why a residential proxy pool or a Scraping API that includes one is the practical baseline.

Build the search URL programmatically and loop over your date and destination combinations, issuing one request per combination. Because the price depends on the exact check-in and check-out, iterate the checkin and checkout parameters to build a rate calendar, and change ss or dest_id to cover more cities. Space the requests and rotate IPs so the batch stays polite and unblocked.

Limited-time ยท 50% off

Get 50% off your first purchase

Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.

Offer ends soon โ€” claim it before it's gone

Claim Discount

About the Author

This guide was written by the SparkProxy Technical Team. SparkProxy builds proxy and web-scraping infrastructure used for price monitoring, travel data scraping, market research, and large-scale data collection. Our products include datacenter proxies, residential proxies, and a managed Scraping API that handles JavaScript rendering, geo-targeting, and anti-bot evasion behind a single request. We write these guides from production experience running collection jobs against exactly the kind of defended, JavaScript-heavy targets covered here. Full API documentation lives at https://www.sparkproxy.io/docs/scraping-api/.

Keep reading

Related articles

How to Scrape Airbnb Listings and Prices

How to Scrape Airbnb Listings and Prices

Learn how to scrape Airbnb listings and prices: extract fields from Airbnb's embedded JSON, handle date-based pricing, map pagination, and anti-bot defenses.

SparkProxyยทGuides
How to Scrape GraphQL APIs

How to Scrape GraphQL APIs

Learn to scrape GraphQL API data: find the /graphql endpoint, read the query and variables in DevTools, then replay your own queries with cursor pagination.

SparkProxyยทGuides
How to Bypass reCAPTCHA When Web Scraping

How to Bypass reCAPTCHA When Web Scraping

How to bypass reCAPTCHA when web scraping the ethical way: how v2 and v3 scoring work, how to raise your reCAPTCHA score, and solvers as a last resort.

SparkProxyยทGuides