DEV Community

Devil Scrapes
Devil Scrapes

Posted on

Booking.com will happily give you a plausible price in the wrong currency

Quick answer

Ask Booking.com for a Paris hotel search priced in USD through the wrong exit geography, and it will not error. It will return HTTP 200, a fully rendered results page, and prices — just in the wrong currency, with no field on the page complaining about it. A price scraper that trusts the first 200 it gets will ship a dataset of plausible, internally-consistent, wrong numbers. The only reliable fix is to read Booking.com's own currency-picker element back off the rendered page and compare it against what you asked for, before you trust a single price on it.

Why doesn't a wrong-currency page just fail? 🌍

When we built the Booking.com Hotel Listings & Reviews Scraper, the working assumption was that geo-mismatches announce themselves — a redirect, an error banner, a locale-selector interstitial. They don't, because there's nothing to announce. Booking.com resolves currency and language from a mix of the request's selected_currency/lang query params, cookies, and the request's apparent geography, and when those signals disagree it just picks one and renders a normal-looking page. $189 and €189 are both syntactically fine floats. Nothing downstream knows which one it got unless you check.

The fix isn't a smarter price parser — a $ symbol alone can't distinguish USD from CAD from AUD anyway. It's reading the page's own admission of what it's showing you:

# actors/booking-hotels-scraper/src/parsers/geo_guard.py
CURRENCY_PICKER_SELECTOR = '[data-testid="header-currency-picker-trigger"]'

def verify_geo_match(html: str, *, expected_currency: str) -> bool:
    displayed = _displayed_currency(html)
    if displayed is None:
        return False  # unverifiable is treated as a mismatch, never a pass
    return displayed == expected_currency.upper()
Enter fullscreen mode Exit fullscreen mode

Booking.com's header currency-picker renders the active currency as a plain 3-letter code — "USD", no symbol, no padding — inside a data-testid element built for their own UI, not for scrapers. That's a far more reliable signal than trying to infer currency from a price string, and it fails closed: if the picker markup isn't found at all, the page is treated as unverified, never waved through as correct by default.

Why does this need a real browser at all? 🛡️

Booking.com fronts its search and hotel pages with a JS-execution bot-control challenge that a bare HTTP request — even with a convincing TLS fingerprint — doesn't clear. This is one of the actors in our fleet that reaches for Camoufox, our anti-detection Firefox fork, specifically because live recon confirmed it renders past that gate on all three page shapes (home, search, detail) where a plain request-based approach couldn't. Browser automation costs meaningfully more compute than an HTTP client, so it's not the default — it's the fallback for targets that actually require it, and Booking.com is one of them.

That decision came with its own trap, worth naming because it's now bitten two other Actors in this fleet before this one: Camoufox's geoip=True option looks like the obvious way to make a launched browser "match" its proxy's exit geography. In practice it makes Camoufox sweep several third-party IP-echo services through that same proxy before the browser process even finishes launching, and on a shared proxy pool that sweep fails often enough to crash the run before a single page loads — with exception classes (InvalidIP, InvalidProxy, LocaleError) that don't inherit from Playwright's own error type, so a catch clause written for playwright.Error lets them straight through uncaught. We use a static locale tag instead and widen the catch explicitly around every launch.

Isn't pinning the proxy's country enough? 🧭

No, and this is the same lesson the currency-picker check encodes at the page level: a residential proxy's exit country is a request, not a guarantee. A geo-random exit can come back 200 OK with data for the wrong market and nothing in the transport layer will tell you. So the currency/locale intent is carried explicitly in the URL itself — a selected_currency and lang query param derived from the requested countryCode — rather than trusted to arrive correctly just because the proxy configuration says "US." The currency-picker check is the second, independent confirmation that the intent actually landed.

The part that generalises 🧭

Any site that localizes by a mix of signals — proxy geography, cookies, query params, Accept-Language — can silently satisfy your request with the wrong locale's answer instead of refusing it. A 200 with a plausible number is a worse failure mode than an error, because nothing downstream flags it. The fix is always the same shape: find the one element the target's own UI uses to display what it decided, and check that against what you asked for — never infer intent from output that could be correct by coincidence.

What the Actor gives you

  • Search mode (destination + dates) or direct-URL mode (hotelUrls) — or both in the same run.
  • Name, address, coordinates, star rating, review score, price, currency, room type, and thumbnail per hotel; optional guest reviews.
  • A currency/locale verification pass on every page before a price is trusted — a mismatched page never emits a wrong-but-plausible row.
  • Per-hotel fault isolation: one dead page or parse error skips that hotel, never the whole run.

Honest limitations 🚧

Hotels only — no Booking.com Attractions, car rentals, or flights. One-shot scrape per run, no price-trend history. Review pagination stays best-effort and never blocks a hotel's core row from landing.

FAQ

Why is a field null on some hotels?
Either Booking.com doesn't expose that field publicly for that hotel, or a currency/locale mismatch made the Actor skip trusting that page's price rather than guess.

Do I need my own proxy?
No — proxy rotation and session handling are inside the Actor.

Can I get reviews without a full re-scrape?
Set scrapeReviews=true and maxReviewsPerHotel; reviews attach to the same run, no separate call needed.

Is this legal?
We only fetch what Booking.com serves on its public search and hotel pages. Match your use case against their terms before commercial use.

Pricing

$0.20 per run, $0.002 per hotel row, $0.0005 per review item (only when reviews are on) — $2.20 per 1,000 hotels. A run that finds nothing costs only the start fee.

Booking.com Hotel Listings & Reviews Scraper on Apify


Built by Devil Scrapes. We handle the bot-control gate, the browser launch traps, and the geo-verification, so your dataset never carries a plausible wrong number. 😈

Top comments (0)