Travel Data Scraping: Hotels, Flights, and Rentals

Scrape hotel rates, flight prices, and vacation rental data from Booking.com, Airbnb, and Kayak with working code and proxy strategies.

Published 9 min read

The travel vertical is one of the most dynamic pricing environments on the web. Hotel nightly rates shift in response to real-time demand signals, airfares change by the second, and vacation rental platforms reprice around local event calendars. That volatility is exactly what makes programmatically collected travel data valuable — to OTAs benchmarking their own inventory, to revenue managers at independent hotels, to airlines monitoring competitor fare strategies, and to analysts tracking supply and occupancy trends at the city level.

This guide covers the travel scraping landscape end to end: what data is available on public pages, why travel sites are particularly hard to collect from, the code patterns that work across lodging and flight targets, and how to architect a price monitor that stays reliable. The per-platform guides linked throughout contain full, ready-to-run code samples in PHP, Node.js, and Rust.

What Travel Data You Can Collect

Travel scraping falls into three categories, each with its own page structure and technical challenges.

Hotel and Accommodation Rates

Hotel property pages on Booking.com and similar OTAs expose data through two structured paths: a schema.org JSON-LD block (typed Hotel or LodgingBusiness) and an embedded page-state object whose internal key paths change silently. The JSON-LD block carries property name, address, star rating, aggregate review score, and price range — a stable parsing target even as the surrounding markup evolves. Nightly rates and room-type breakdowns live in the embedded state, which is data-rich but structurally fragile.

Full technical guide with PHP, Node.js, and Rust code: How to Scrape Booking.com.

Vacation Rental Listings

Airbnb listing pages ship two parseable sources in the rendered HTML: a LodgingBusiness JSON-LD block and a __NEXT_DATA__ Next.js state object that carries detailed pricing, amenity lists, and review metadata. Neither source is available from a plain HTTP GET — the page requires full JavaScript rendering before either appears. Availability calendars are fetched asynchronously and require a separate interception approach or the platform's iCal export where available.

Full technical guide with PHP, Node.js, and Rust code: How to Scrape Airbnb.

Flight Prices

Kayak flight search results arrive through a React SPA. The rendered page carries both a sparse JSON-LD block (breadcrumb and page-context metadata) and a __NEXT_DATA__ state object with initial price nodes — though key paths update without notice. DOM-level price extraction after full JavaScript rendering is the most reliable approach; the proxy layer described below handles that rendering automatically.

Full technical guide with PHP, Node.js, and Rust code: How to Scrape Kayak Flight Prices.

Why Travel Sites Are Hard Scraping Targets

Travel platforms share a cluster of anti-scraping defenses that go well beyond a simple IP block:

JavaScript rendering is required. Airbnb and Kayak are React/Next.js applications. The initial server response is a skeletal HTML shell; prices and availability appear only after client-side hydration. A plain HTTP client returns empty content regardless of IP quality.

CDN-level datacenter blocking. Booking.com, Airbnb, and Kayak all reject known cloud and datacenter IP ranges before serving content. Residential or ISP proxies with realistic TLS fingerprints are the entry requirement, not an optimization.

Geo-IP pricing. Hotel rates and airline fares are personalised by visitor country and currency. A scraper routing through mixed geographies collects incoherent rate data. Targeted geo proxies — US IPs for US fares, UK IPs for UK rates — are necessary for data you can meaningfully compare across runs.

Bot scoring layers. Booking.com uses DataDome, which correlates requests by cookie, referer, timing patterns, TLS fingerprint, and behavioral signals in combination. High-volume collection without realistic browsing cadence triggers CAPTCHAs and session resets.

See How to Avoid Getting Blocked and Proxy Types Explained for detailed countermeasures.

Prerequisites

All code in this guide routes requests through the Bright Data Web Unlocker, which bundles JavaScript rendering, residential IP rotation, TLS fingerprint spoofing, and CAPTCHA solving into a single proxy endpoint:

export PROXY_URL="http://brd-customer-<id>-zone-<unblocker_zone>:<password>@brd.superproxy.io:22225"

No Bright Data account yet? Explore Bright Data's travel data collection tools →

Parsing Lodging JSON-LD Across Travel Sites

JSON-LD is the most stable parsing target across hotel and vacation rental platforms. Booking.com, Airbnb, and most independent OTAs emit a schema.org LodgingBusiness or Hotel block that changes far less often than their embedded JavaScript state. The samples below extract core fields from any lodging JSON-LD response and serve as a starting point before you add the site-specific state parsing shown in the per-platform guides.

PHP

<?php
// Run: php lodging.php "https://www.booking.com/hotel/gb/example.html"
$proxy = getenv('PROXY_URL');
$url   = $argv[1] ?? 'https://www.booking.com/hotel/gb/example.html';

$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_PROXY          => $proxy,
    CURLOPT_SSL_VERIFYPEER => false,
    CURLOPT_TIMEOUT        => 60,
    CURLOPT_HTTPHEADER     => [
        'Accept-Language: en-US,en;q=0.9',
        'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    ],
]);
$html = curl_exec($ch);
curl_close($ch);

$doc = new DOMDocument();
@$doc->loadHTML($html);
$xp = new DOMXPath($doc);

$lodging = null;
foreach ($xp->query('//script[@type="application/ld+json"]') as $node) {
    $ld   = json_decode($node->textContent, true);
    $type = $ld['@type'] ?? '';
    if ($type === 'LodgingBusiness' || $type === 'Hotel') { $lodging = $ld; break; }
}

echo json_encode([
    'name'       => $lodging['name']             ?? null,
    'address'    => $lodging['address']           ?? null,
    'priceRange' => $lodging['priceRange']        ?? null,
    'starRating' => $lodging['starRating']        ?? null,
    'rating'     => $lodging['aggregateRating']   ?? null,
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), PHP_EOL;

Node.js

// lodging.mjs — node lodging.mjs "https://www.booking.com/hotel/gb/example.html"
// Install: npm i axios https-proxy-agent cheerio
import axios from 'axios';
import { HttpsProxyAgent } from 'https-proxy-agent';
import * as cheerio from 'cheerio';

const agent = new HttpsProxyAgent(process.env.PROXY_URL);
const url   = process.argv[2] ?? 'https://www.booking.com/hotel/gb/example.html';

const { data: html } = await axios.get(url, {
  httpsAgent: agent, proxy: false, timeout: 60_000,
  headers: {
    'Accept-Language': 'en-US,en;q=0.9',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
  },
});

const $ = cheerio.load(html);
let lodging = null;
$('script[type="application/ld+json"]').each((_, el) => {
  try {
    const ld = JSON.parse($(el).text());
    if (ld['@type'] === 'LodgingBusiness' || ld['@type'] === 'Hotel') lodging = ld;
  } catch { /* skip malformed blocks */ }
});

console.log(JSON.stringify({
  name:       lodging?.name             ?? null,
  address:    lodging?.address          ?? null,
  priceRange: lodging?.priceRange       ?? null,
  starRating: lodging?.starRating       ?? null,
  rating:     lodging?.aggregateRating  ?? null,
}, null, 2));

Rust

// Cargo.toml:
//   reqwest = { version = "0.12", features = ["blocking"] }
//   scraper = "0.20"
//   serde_json = "1"
use scraper::{Html, Selector};
use serde_json::Value;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let url = std::env::args().nth(1)
        .unwrap_or_else(|| "https://www.booking.com/hotel/gb/example.html".into());

    let client = reqwest::blocking::Client::builder()
        .proxy(reqwest::Proxy::all(std::env::var("PROXY_URL")?)?)
        .danger_accept_invalid_certs(true)
        .build()?;

    let html = client
        .get(&url)
        .header("Accept-Language", "en-US,en;q=0.9")
        .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
        .send()?
        .text()?;

    let doc = Html::parse_document(&html);
    let sel = Selector::parse(r#"script[type="application/ld+json"]"#).unwrap();

    let mut lodging = Value::Null;
    for el in doc.select(&sel) {
        let raw = el.text().collect::<String>();
        if let Ok(ld) = serde_json::from_str::<Value>(&raw) {
            let t = ld["@type"].as_str().unwrap_or("");
            if t == "LodgingBusiness" || t == "Hotel" { lodging = ld; break; }
        }
    }

    println!("{}", serde_json::to_string_pretty(&serde_json::json!({
        "name":       lodging["name"],
        "address":    lodging["address"],
        "priceRange": lodging["priceRange"],
        "starRating": lodging["starRating"],
        "rating":     lodging["aggregateRating"],
    }))?);
    Ok(())
}

The per-platform guides extend this pattern with site-specific data sources — Airbnb's __NEXT_DATA__ for nightly pricing, Booking.com's embedded room-state object for rate and cancellation breakdowns, and Kayak's rendered DOM for per-itinerary fare detail.

Architecture for a Travel Price Monitor

A travel price monitor follows the same loop as any other data tracker, with a few domain-specific additions:

  1. Geo-targeted fetch. Route each request through a proxy IP in the target market (US IPs for US fares, UK IPs for UK hotel rates). The same property URL returns different pricing to different geographies — mixing proxy origins makes the data incomparable.
  2. Parse the stable source first. JSON-LD gives you name, address, star rating, and price range reliably. If that succeeds, attempt the fragile embedded-state parse for richer detail. Log failures per-source separately so you can distinguish JSON-LD drift from embedded-state drift.
  3. Store time series, not snapshots. Append {property_id, source, price, currency, scraped_at} on every run; never overwrite. Travel prices are only useful as a time series — a single observation tells you nothing about whether a rate is low.
  4. Alert on null rate spikes. Travel sites update their markup frequently. Track the fraction of requests returning a null price per field; a sudden jump to near-100% means the site structure changed, not that all properties became free.
  5. Schedule tightly for flights, loosely for hotels. Airfares can move meaningfully within minutes around booking events; hotel rates are typically stable enough that hourly collection captures useful signal without burning through proxy budget.

For additional storage and change-detection guidance, see the E-commerce Web Scraping overview — the pattern transfers directly to travel data.

Travel data scraping operates in a well-defined public information space — prices and property details visible to any anonymous visitor — but the platforms' Terms of Service uniformly prohibit automated access. Those are separate questions: whether public-data scraping is lawful in your jurisdiction is a legal question your counsel should answer; whether it violates a platform contract is a separate matter with its own consequences. A few practical principles apply regardless:

  • Public data only. Rates, availability, and property details visible without a login are a different category from anything behind an authentication wall. Don't scrape logged-in sessions.
  • Respect personal data rules. Host names and profile photos on vacation rental platforms may qualify as personal data under GDPR or CCPA even when published publicly. Apply appropriate handling and retention policies.
  • Throttle and be a good citizen. Request rates that degrade the site for real users create liability and accelerate blocks. Scrape at a pace that is invisible in aggregate traffic.

Scaling to Production

Monitoring hundreds of hotel properties, tracking fares across dozens of routes, or running a city-level vacation rental market model requires infrastructure that compounds quickly: residential IP rotation, JavaScript rendering capacity, CAPTCHA handling, geo-targeted proxies, and structural-change monitoring all need maintenance. Bright Data's travel data collection tools handle that infrastructure layer, returning clean structured data on demand. For large historical datasets, pre-built datasets are often faster to acquire than bootstrapping a full scraper from scratch.

Related: Bright Data Web Unlocker review, How to Avoid Getting Blocked, Proxy Types Explained, and E-commerce Scraping overview.

Collect travel pricing data at scale with Bright Data →

FAQ

Can I scrape Booking.com and Airbnb legally?

Scraping publicly visible pricing data sits in a contested legal space. The data is accessible to any anonymous visitor, but both platforms prohibit automated access in their Terms of Service. Laws on scraping public data vary by jurisdiction — get qualified legal advice before deploying a commercial scraper against either platform.

Why do travel sites block datacenter proxies?

Airbnb, Booking.com, and Kayak all implement CDN-level or bot-management-layer blocks that reject known cloud and datacenter IP ranges before any useful content loads. Residential or ISP proxies with realistic TLS fingerprints and browser-consistent headers are the minimum viable starting point.

Do I need a headless browser to scrape travel sites?

For sites like Airbnb and Kayak that hydrate content client-side, yes — a fully rendered page is required to access price data. The Bright Data Web Unlocker handles JavaScript rendering automatically, so you supply a plain HTTP client and receive the rendered HTML without managing a headless browser yourself.

How often do travel site page structures change?

Booking.com and Airbnb update their embedded JSON key paths silently and frequently, often tied to A/B experiments or framework updates. JSON-LD blocks typed as LodgingBusiness or Hotel change far less often than embedded state objects. Monitor your null rate per field; a spike from near-zero to 100% signals a markup change requiring attention.

Advertisement