Booking.com Scraper โ Rate & Value Report
Pricing
from $4.99 / 1,000 results
Booking.com Scraper โ Rate & Value Report
๐จ BookingScraper extracts real-time Booking.com listings data โ prices, availability, ratings, reviews, amenities & location. โก Ideal for market research, price monitoring, travel analytics, and lead gen. ๐ Fast, accurate, anti-blocking scraping with CSV/JSON export.
Pricing
from $4.99 / 1,000 results
Rating
0.0
(0)
Developer
API Empire
Maintained by CommunityActor stats
0
Bookmarked
5
Total users
1
Monthly active users
15 days ago
Last modified
Categories
Share
Booking.com Scraper โ Hotels, Room Rates and Value Scores as JSON
Booking.com Scraper โ Rate & Value Report extracts three linked entity types from public Booking.com pages in one run: property records (name, stars, guest rating, facilities, address, trader identity), per-room rate options (base price, discounted price, discount %, taxes and fees, board type, refundable flag), and a derived value ranking that scores each property on rating against price and re-orders the report best-value first. Every row is typed, normalized JSON โ no HTML, no selectors, no parsing. By the end of this page you will know which fields you get, what unlocks each of them, and where the honest gaps are.
What is Booking.com Scraper โ Rate & Value Report?
Booking.com Scraper โ Rate & Value Report is an Apify Actor that turns a destination name or a list of Booking.com URLs into a ranked accommodation dataset. It scrapes public search-results and hotel detail pages, then adds two derived layers: a value score (valueScore, valueRatio, valueRank) computed across the whole result set, and a per-room rate breakdown parsed from Booking's own room blocks.
No Booking.com account, cookie, or login is required. The Actor never authenticates โ it fetches the same pages an anonymous visitor sees. It reads no member-only rates, touches no booking or payment flow, and performs no in-platform action.
- Scrape hotels and other accommodation โ hotels, apartments, hostels, villas, resorts, campsites and 9 more categories via
stayCategory - Scrape per-room rate options โ
basePrice,discountedPrice,discountPct,taxesAndFees,totalPrice,boardType,refundable - Scrape derived value rankings โ
valueScore(0โ100 percentile),valueRank(1 = best rating-for-price),valueRatio(rawrating รท price) - Export as JSON, CSV, Excel, XML or HTML from the Apify dataset, or read it straight from the Apify API
- No proxy management and no parsing โ a residential proxy ladder and two independent fetch engines are built in
What data does the Rate & Value Report collect?
One run returns a single dataset item per property, and each item carries the property record, its room inventory, its rate options, its review breakdown, its trader identity block, and the derived value fields. Forty top-level keys are written on every row.
| Data Type | Key Fields | JSON Field Names |
|---|---|---|
| Property record | Name, Booking hotel id, star class, accommodation type, description, canonical URL | name, hotelId, stars, type, description, url |
| Value ranking (derived) | Percentile value score, value rank, raw rating-per-price ratio, output position | valueScore, valueRank, valueRatio, order |
| Room rate options | Pre-discount price, discounted price, discount %, taxes and fees, total, board type, refundability | rateBreakdown[] โ .basePrice, .discountedPrice, .discountPct, .taxesAndFees, .totalPrice, .boardType, .refundable, .currency, .roomId, .roomName |
| Room inventory | Room id, name, description, size, max occupancy, cheapest block price | rooms[] โ .id, .name, .description, .size, .occupancy, .price |
| Guest ratings | Score, localized label, review count, per-category subscores | rating, ratingLabel, reviews, categoryReviews[].name, categoryReviews[].score |
| Facilities and highlights | Facility names and ids, marketing highlights, breakfast availability | facilities[].name, facilities[].id, highlights, breakfast |
| Location and address | Latitude, longitude, street, city, country code, breadcrumb trail | location.lat, location.lng, address.full, address.city, address.country, breadcrumbs[] |
| House rules and fine print | Check-in window, check-out window, house-rule lines, fine print | checkIn, checkOut, policies, finePrint |
| Business / trader identity | Trader flag, legal company name, email, phone, registration number, postal address | traderInfo โ .isBusiness, .companyName, .email, .phone, .registrationNumber, .tradeRegisterName, .address.* |
| Host, chain and licence | Host name, hotel chain, first published licence number | hostInfo, hotelChain, licenseInfo |
| Media | Hero image, deduplicated gallery (capped at 50), per-room photo sets | image, images, roomImages[].roomId, roomImages[].images |
| Run metadata | Scrape timestamp, queued URL, originating query, resolved stay dates, price, currency | timeOfScrapeISO, source_url, startUrlOrQuery, checkInDate, checkOutDate, price, currency |
Need more accommodation and local-market data?
API Empire does not currently publish a second Booking.com Actor, so there is no companion review or availability scraper for this platform to point you at. For adjacent work, the Apartments.com Scraper โ Neighborhood Rent & Livability Report covers long-stay rental pricing in the same value-report style, and the Google Maps Scraper with Lead Contact Enrichment is the usual pairing when you need on-the-ground contact details for the properties you just ranked.
How does this differ from the official Booking.com Demand API?
Booking.com's own programmatic surface is the Demand API at developers.booking.com; access is granted through an approved affiliate or distribution partnership with a signed agreement, and the endpoints are built for partners who send bookings back to Booking.com. This Actor is the opposite shape: a read-only extractor over public pages that anyone with an Apify account can start immediately.
| Feature | Booking.com Demand API | Rate & Value Report |
|---|---|---|
| Access | Approved partnership and signed agreement required, per Booking.com's published Demand API onboarding docs | Open โ start the Actor from the Apify Console or Apify API with your Apify token |
| Intended purpose | Distributing and booking inventory as a Booking.com partner | Read-only extraction and analysis of public listing data |
| Value scoring | Not provided โ you receive raw availability and pricing | valueScore, valueRank, valueRatio computed across the whole result set |
| Discount visibility | Rate plans as contracted, in partner terms | basePrice vs discountedPrice and a derived discountPct as shown publicly on the page |
| Trader / legal identity | Not the API's concern | traderInfo, licenseInfo, hostInfo as published on the listing |
| Setup time | Partner onboarding and review | Fill the input form, press Start |
| Output shape | Partner-contract JSON schema | 40 stable top-level keys per property, identical on every run |
Use the Demand API when you are an approved partner and your goal is to sell inventory โ it is the only lawful path to real booking capability. Use this Actor when your goal is analysis: rating against price, published discounts and taxes, or a structured accommodation dataset built from pages that are already public.
Why do developers and teams scrape Booking.com?
Booking.com is the largest single public catalogue of accommodation pricing, and almost every price the platform shows is decorated with a discount claim, a tax note and a board type that only exist inside the rendered page. Four groups get real leverage from turning that into rows.
For AI engineers and agent builders
A travel agent answering "find me a well-rated place in Lisbon under โฌ150 that includes breakfast" needs structure, not a screenshot. Run the Actor with location, stayCheckIn, stayCheckOut and priceBand, then hand the dataset to the model: valueScore is a ready ranking signal, rateBreakdown[].boardType answers the breakfast constraint without string matching, and rateBreakdown[].refundable answers the cancellation constraint. Every field is typed JSON, so there is no HTML parsing inside the agent loop โ the most common reliability failure in tool-using agents.
For travel marketers and deal sites
Deal editors need the properties whose rating is disproportionate to their price, not the cheapest ones. Set sortResultsBy to bestValue and the report arrives pre-ranked: valueRank: 1 is the strongest rating-for-price in the set, and valueRatio exposes the raw number for re-weighting. discountPct on each rate option separates a real markdown from a strikethrough that never moved, and ratingLabel gives the exact wording Booking shows guests in the language set by resultLanguage.
For researchers and market analysts
Everything returned is drawn from pages Booking.com serves to anonymous visitors โ no login, no member rate, no reviewer personal data, no booking-flow content. That matters for pricing studies, tourism-economics work and accommodation-supply research where sample provenance is part of the method. A typical study sweeps one location per city with fixed stayCheckIn/stayCheckOut and maxProperties, then compares price, stars, rating and taxesAndFees across markets. timeOfScrapeISO timestamps every observation, so repeated runs assemble into a panel.
For developers building data products
The Actor is a scheduled job with a stable output contract, which is what a pipeline needs. Point a daily Apify schedule at a saved input, read defaultDatasetId from the run object, and upsert on hotelId plus checkInDate. Field names and types do not change between runs, so a warehouse table defined once keeps loading. Pay-per-event pricing means a run's cost tracks the rows you actually got, which makes per-customer cost modelling straightforward.
๐ How to scrape Booking.com hotel rates (step by step)
The Actor runs on the Apify platform only. There is no separate signup, no API key of its own, and no credit system โ you start it from the Apify Console or call it with the Apify API using your Apify token.
- Open the Actor on its Apify Store listing and press Try for free, or open it in the Apify Console if it is already in your account.
- Give it a target. No field is required by the schema, but the run does nothing without one of the two entry points: a city, region or landmark in
location, or Booking.com URLs instayUrls(hotel detail pages andsearchresultspages are both accepted). - Set the stay dates. Fill
stayCheckInandstayCheckOut(YYYY-MM-DD, or a relative form like2 weeks). This unlocksrateBreakdown,taxesAndFeesand correct currency handling โ without both dates Booking serves no priced availability blocks. - Choose filters and ordering.
maxPropertiescaps the sample,sortResultsBysets the output order (bestValueby default), andstayCategory,starRating,minGuestRating,priceBandandsearchOrdernarrow the comparison set before ranking. - Start the run, then export. Open the Output tab โ the default ๐ Value-ranked hotels view leads with the ranking columns โ and export to JSON, CSV, Excel, XML or HTML, or pull the dataset over the Apify API.
What to do when Booking.com changes its structure
Nothing on your side. The Actor reads four independent layers of the page โ Apollo store, JSON-LD, inline window.booking variables, DOM selectors โ and merges whatever each yields, so a change to one layer degrades single fields rather than breaking the run. Output key names and types are the integration contract and are kept stable while the scraper is maintained against Booking's markup.
What changed in Booking.com scraping recently?
The largest structural shift is regulatory rather than technical: the European Commission designated Booking.com a gatekeeper under the Digital Markets Act on 13 May 2024, with compliance obligations applying from 13 November 2024, which reshaped how search results and ranking disclosures render in the EU.
- Trader-identity blocks became routine. The EU Digital Services Act's trader-traceability obligation has applied to online marketplaces since 17 February 2024, and Booking now publishes legal-entity, registration and contact data on many listings. That is exactly the block this Actor reads into
traderInfo,licenseInfoandhostInfoโ data that simply was not on the page for most listings before. - Two rendering paths coexist. Priced availability appears in the React/Apollo store on some responses and in the legacy server-rendered
#hprt-tablerooms table on others. A scraper that knows only one of them returns an emptyrateBreakdownon the other. This Actor tries the Apollo path first and falls back to the DOM table. - Anti-bot on the search-results page is aggressive. A competing Booking.com Actor's own listing states plainly that "Booking.com blocks shared Apify proxies" (plowdata/booking-com-review-scraper, checked on the Apify Store 2026-07-25 โ not measured here). This Actor defaults to Apify RESIDENTIAL proxies and steps down a ladder on repeated blocks.
- What is still public: search results, hotel detail pages, published rates, guest scores, facilities, house rules and trader blocks. What is not: member and loyalty-gated rates, anything behind login, and the booking or payment flow.
Maintenance continues against whichever rendering path Booking ships next; the layered extractor is what makes that survivable.
โฌ๏ธ Input
Every parameter below comes from .actor/actor.json. Nothing is required โ but a run with neither location nor stayUrls logs "No destination and no Stay URLs. Nothing to do." and exits without pushing rows. Each field also accepts the base Booking scraper's key name as an alias, so an input written for the base Actor runs here unchanged.
| Parameter | Required | Type | Description | Example Value |
|---|---|---|---|---|
location | No | string | Destination to value-rank โ city, region or landmark. Ignored when stayUrls is non-empty. Default "", editor textfield. Alias: search. | "Amsterdam" |
stayUrls | No | array | Booking.com URLs โ hotel detail pages and/or searchresults pages. Non-Booking URLs are dropped. Default [], editor stringList. Alias: startUrls (also accepts urls); string or {"url": โฆ} entries. | ["https://www.booking.com/hotel/nl/example.html"] |
maxProperties | No | integer | How many properties to scrape and rank per destination / URL. Default 10, minimum 1, maximum 20000. Alias: maxItems. | 15 |
sortResultsBy | No | string | Output ordering. Enum: bestValue, lowestPrice, highestRating, asFound. Default bestValue, editor select. Any unrecognized value falls through to bestValue. | "bestValue" |
includeRateBreakdown | No | boolean | Parse per-room rate options from Booking's room blocks. Default true. Needs both stay dates to return anything. | true |
includePropertyDetails | No | boolean | Include rooms, facilities, highlights, images, room photos, breadcrumbs, chain, licence, host and trader info. Default true. Alias: scrapeAdditionalHotelData. | true |
stayCheckIn | No | string | Check-in. Absolute YYYY-MM-DD or relative (2 weeks). Default "", editor datepicker, dateType: absoluteOrRelative, pattern ^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$|^(\d+)\s*(day|week|month|year)s?$|^$. Alias: checkIn. | "2026-09-12" |
stayCheckOut | No | string | Check-out. Same format, same pattern, same editor as stayCheckIn. Default "". Alias: checkOut. | "2026-09-15" |
dateFlexibility | No | string | Shift the stay dates by up to this many days. Enum: 0, 1, 2, 3, 7. Default "0", editor select. Sent as flex_window only when not "0". Alias: flexWindow. | "0" |
guestsAdults | No | integer | Adult guests the rate is quoted for. Default 2, minimum 1, maximum 30. Alias: adults. | 2 |
guestsChildren | No | integer | Children the rate is quoted for. Default 0, minimum 0, maximum 30. Alias: children. | 0 |
roomsNeeded | No | integer | Rooms for the stay. Default 1, minimum 1, maximum 9. Alias: rooms. | 1 |
stayCategory | No | string | Accommodation type filter. Enum: none, hotels, apartments, hostels, guest houses, homestays, bed and breakfasts, holiday homes, boats, villas, motels, resorts, holiday parks, campsites, luxury tents. Default "none", editor select. Applied only on destination searches. Alias: propertyType. | "hotels" |
searchOrder | No | string | Booking's server-side result order, applied before value re-ranking. Enum: distance_from_search, price, review_score_and_price, review_score, class. Default "review_score_and_price", editor select. Alias: sortBy. | "review_score_and_price" |
minGuestRating | No | string | Guest-rating floor, e.g. 8. Default "", editor textfield. Truncated to a whole number and sent as review_score=<nร10>, so 8.5 behaves as 8. Alias: minimumRating. | "8" |
starRating | No | string | Official star class filter. Enum: any, 1, 2, 3, 4, 5. Default "any", editor select. Alias: starsCountFilter. | "any" |
priceBand | No | string | Nightly price band, min-max (60-250) or min+ (100+). Default "0-999999", editor textfield; the default is treated as "no filter". Malformed values are ignored silently. Alias: minMaxPrice. | "60-250" |
resultCurrency | No | string | Currency for prices and the rate breakdown. Enum: USD, EUR, GBP, CAD, AUD, CHF, JPY, CNY, INR, BRL, MXN. Default "EUR", editor select. Alias: currency. | "EUR" |
resultLanguage | No | string | Booking interface language, which also selects the searchresults.<lang>.html domain path. Enum: en-gb, en-us, de, fr, es, it, pt-br, nl, pl, ru, ja, zh. Default "en-gb", editor select. Alias: language. | "en-gb" |
proxyConfiguration | No | object | Apify proxy settings, editor proxy. Default {"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]}. apifyProxyCountry is carried through to the residential tier when set. | {"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]} |
Example input
{"location": "Amsterdam","stayUrls": [],"maxProperties": 15,"sortResultsBy": "bestValue","includeRateBreakdown": true,"includePropertyDetails": true,"stayCheckIn": "2026-09-12","stayCheckOut": "2026-09-15","dateFlexibility": "0","guestsAdults": 2,"guestsChildren": 0,"roomsNeeded": 1,"stayCategory": "hotels","searchOrder": "review_score_and_price","minGuestRating": "8","starRating": "any","priceBand": "60-250","resultCurrency": "EUR","resultLanguage": "en-gb","proxyConfiguration": {"useApifyProxy": true,"apifyProxyGroups": ["RESIDENTIAL"]}}
Most common input mistake: leaving stayCheckIn and stayCheckOut empty while expecting the rate breakdown. Booking only injects priced availability blocks for a specific stay, so a dateless run returns rateBreakdown: [], no taxesAndFees, and prices in Booking's own default currency rather than your resultCurrency โ because the currency parameter is only appended to detail-page URLs when both dates are present. The Actor logs a warning for exactly this case: "Rate breakdown requested but no check-in/out dates set."
โฌ๏ธ Output
Results land in the Actor's default Apify dataset as typed, normalized JSON with a stable key set โ 40 top-level keys on every row, in the same order, whether or not each has a value. Export from the Console or the API as JSON, CSV, Excel (XLSX), XML, HTML or JSONL. The default view, ๐ Value-ranked hotels, surfaces all 40 keys with the ranking columns first; nothing is hidden behind it. Missing data is null (or [] / "") โ the extractor never substitutes a plausible-looking value for one it could not find.
Scraped property with value score and rate breakdown
{"order": 1,"valueRank": 1,"valueScore": 100.0,"valueRatio": 0.023133,"name": "Canal House Amsterdam","price": 372.0,"currency": "EUR","rating": 8.6,"ratingLabel": "Fabulous","stars": 4,"rateBreakdown": [{"roomId": "12345678_401234567_2_0_0","roomName": "Deluxe King Room with Canal View","boardType": "Breakfast included","basePrice": 465.0,"discountedPrice": 372.0,"discountPct": 20.0,"taxesAndFees": 44.64,"totalPrice": 416.64,"currency": "EUR","refundable": true},{"roomId": "12345678_401234567_2_1_0", "roomName": "Standard Twin Room","boardType": null, "basePrice": null, "discountedPrice": 318.0,"discountPct": null, "taxesAndFees": 38.16, "totalPrice": 356.16,"currency": "EUR", "refundable": false}],"reviews": 1487,"breakfast": "Available","url": "https://www.booking.com/hotel/nl/canal-house-amsterdam.en-gb.html","startUrlOrQuery": "Amsterdam","type": "hotel","description": "Set in a 17th-century canal house in the Jordaan district โฆ","checkInDate": "2026-09-12","checkOutDate": "2026-09-15","checkIn": "From 15:00 to 23:00","checkOut": "From 07:00 to 11:00","location": { "lat": 52.374, "lng": 4.8897 },"address": { "full": "Keizersgracht 148", "country": "NL", "city": "Amsterdam" },"image": "https://cf.bstatic.com/xdata/images/hotel/max1024x768/000000001.jpg","rooms": [{"id": "12345678_401234567","name": "Deluxe King Room with Canal View","description": "This double room features a seating area, minibar and canal views.","size": 28,"occupancy": 2,"price": 372.0}],"highlights": ["Free WiFi", "Canal view", "Very good breakfast"],"finePrint": "The property is located in a listed building without a lift.","policies": ["Pets: not allowed", "Smoking: not allowed"],"images": ["https://cf.bstatic.com/xdata/images/hotel/max1024x768/000000001.jpg"],"roomImages": [{ "roomId": "401234567", "images": ["https://cf.bstatic.com/โฆ/000000011.jpg"] }],"categoryReviews": [{ "name": "Cleanliness", "score": 8.9 },{ "name": "Location", "score": 9.4 }],"hotelChain": null,"licenseInfo": "0363 1234 5678 ABCD","hostInfo": null,"traderInfo": {"isBusiness": true, "email": "info@example-hotel.nl", "phone": "+31201234567","companyName": "Example Hospitality B.V.", "firstName": null, "middleName": null,"lastName": null, "registrationNumber": "12345678","tradeRegisterName": "Kamer van Koophandel","address": {"street": "Keizersgracht 148", "street2": null, "postalCode": "1015 CX","city": "Amsterdam", "countryCode": "NL", "state": null}},"breadcrumbs": [{ "name": "Netherlands", "url": "https://www.booking.com/country/nl.html" },{ "name": "Amsterdam", "url": "https://www.booking.com/city/nl/amsterdam.html" }],"hotelId": "12345678","facilities": [{ "name": "Free WiFi", "id": 107 }, { "name": "Bar", "id": 11 }],"timeOfScrapeISO": "2026-07-25T09:41:12.883410+00:00","source_url": "https://www.booking.com/hotel/nl/canal-house-amsterdam.html"}
Every output key, and what fills it
| Field | Type | Notes |
|---|---|---|
order | integer | 1-based position in the final report, rewritten after re-ranking so it always matches sortResultsBy. |
valueRank | integer | null | 1 = highest rating รท price in the run. null without a rating or a price. |
valueScore | number | null | Percentile of rating รท price across scored rows, 0โ100, one decimal. Lowest scored row is 0.0, highest 100.0; a lone scoreable row gets 100.0. |
valueRatio | number | null | Raw rating รท price, 6 decimals. |
name | string | Property name. A page yielding no name is retried then skipped, so this is never null on a pushed row. |
price | number | null | Lowest priced option on the page โ minimum of rooms[].price and rateBreakdown[].discountedPrice, falling back to Booking's inline b_cheapest_price_that_fits_search_eur and then the DOM price element. |
currency | string | The property's b_hotel_currencycode, defaulting to "USD". Its listing currency โ not necessarily the currency price is quoted in (see caveats). |
rating | number | null | Guest score out of 10, from ReviewScoreSummary or JSON-LD aggregateRating.ratingValue. |
ratingLabel | string | null | Booking's localized label; if absent, derived from the score โ Superb โฅ 9, Very good โฅ 8, Good โฅ 7, Pleasant โฅ 6, else null. |
stars | number | null | Official star class from the StarRating object. |
rateBreakdown | array | Per-room rate options. [] when includeRateBreakdown is false, when no stay dates are set, or when no priced block exists. Never fabricated. |
rateBreakdown[].roomId | string | null | Apollo room id, or the DOM table's data-block-id. |
rateBreakdown[].roomName | string | null | Room type name as displayed. |
rateBreakdown[].boardType | string | null | Booking's meal-plan value, or normalized from the meal-plan cell into All inclusive, Full board, Half board, Breakfast included, Breakfast available, Dinner included, Lunch included. |
rateBreakdown[].basePrice | number | null | Pre-discount / strikethrough price, when published. |
rateBreakdown[].discountedPrice | number | null | The price actually charged for this option. |
rateBreakdown[].discountPct | number | null | (basePrice โ discountedPrice) / basePrice ร 100, one decimal. Only when both exist and basePrice โฅ discountedPrice. |
rateBreakdown[].taxesAndFees | number | null | Excluded charges as published under the price. |
rateBreakdown[].totalPrice | number | null | discountedPrice + taxesAndFees, 2 decimals. null when taxes are absent or published as zero. |
rateBreakdown[].currency | string | null | From Booking's price object; on the DOM path it is backfilled with your resultCurrency. |
rateBreakdown[].refundable | boolean | null | true on free cancellation, false on non-refundable, null when neither is stated. |
reviews | integer | Review count; 0 when unavailable. |
breakfast | string | null | "Available" when breakfast appears in the facility/highlight set or in Booking's "breakfast is available/included" copy, else null. |
url | string | Canonical URL from JSON-LD, falling back to the fetched URL. |
startUrlOrQuery | string | Your location value, or the URL on a URL-driven run โ the grouping key for multi-destination datasets. |
type | string | Booking's b_hotel_type_anchor, defaulting to "hotel". |
description | string | null | JSON-LD description, falling back to the Apollo description. Not gated by includePropertyDetails. |
checkInDate / checkOutDate | string | null | The resolved stay dates for this run, YYYY-MM-DD. null on a dateless run. |
checkIn / checkOut | string | null | The property's house-rule time windows, e.g. "From 15:00 to 23:00". A one-sided window renders as "From 15:00" or "Until 11:00" โ never a half-formed "to None". |
location | object | { "lat": number|null, "lng": number|null }, from Apollo data or an inline-variable regex fallback. |
address | object | { "full", "country", "city" }; country upper-cased. Empty strings, not null, when unknown. |
image | string | First gallery image, or "". |
rooms | array | { id, name, description, size, occupancy, price } per room type. [] when includePropertyDetails is false. |
highlights | array | Booking's facility-highlight titles. [] when includePropertyDetails is false. |
finePrint | string | Fine-print paragraphs joined with spaces; "" when none. Not gated by includePropertyDetails. |
policies | array | null | Real house-rule lines only ("Pets: not allowed"), from house-rule objects and boolean rule flags. null when none are published โ never invented from marketing copy. |
images | array | Deduplicated gallery URLs, capped at 50. [] when includePropertyDetails is false. |
roomImages | array | { roomId, images[] } per room photo set. [] when includePropertyDetails is false. |
categoryReviews | array | { name, score } subscores such as Cleanliness, Location, Staff; the hotel_ prefix is stripped and the name title-cased. Not gated by includePropertyDetails. |
hotelChain | string | null | Chain name when the property belongs to one. null when includePropertyDetails is false. |
licenseInfo | string | null | First published licence/registration number only. |
hostInfo | string | null | Host profile name; falls back to the trader's first + last name when no company name is published. |
traderInfo | object | null | isBusiness, email, phone, companyName, firstName, middleName, lastName, registrationNumber, tradeRegisterName, plus nested address with street, street2, postalCode, city, countryCode, state. null when includePropertyDetails is false. |
breadcrumbs | array | { name, url } for Booking's country/region/city trail. [] when includePropertyDetails is false. |
hotelId | string | null | Booking's numeric hotel id, as a string. |
facilities | array | { name, id } per facility; the DOM fallback returns id: null. [] when includePropertyDetails is false. |
timeOfScrapeISO | string | UTC ISO-8601 timestamp of the extraction. |
source_url | string | The URL as queued, before availability parameters were appended โ the stable join key across runs. |
Scraped rate option (standalone shape)
rateBreakdown is the second entity in the report and has its own schema, identical whether it came from the Apollo store or the legacy rooms table:
{"roomId": "12345678_401234567_2_0_0","roomName": "Deluxe King Room with Canal View","boardType": "Breakfast included","basePrice": 465.0,"discountedPrice": 372.0,"discountPct": 20.0,"taxesAndFees": 44.64,"totalPrice": 416.64,"currency": "EUR","refundable": true}
Limits, caveats and known behaviour
These are the things worth knowing before you build on the output. All of them are visible in the source.
pricefollows the stay, not the night. On a multi-night search Booking renders the price for the whole stay in the rooms table, andpriceis the minimum of those figures โ so on a 3-night runpriceis the 3-night total for the cheapest option, even though the dataset view labels the column Nightly Price. Within one run every row uses the same stay length, so the ranking is internally consistent; keep the stay length fixed when comparingvalueRatioacross runs.currencyand the price currency can disagree.currencycomes from the property's ownb_hotel_currencycodeand defaults to"USD". YourresultCurrencyreaches Booking asselected_currency, which is appended to detail URLs only when both stay dates are set. On a dateless run, prices come back in whatever currency Booking picks for the locale. Set both dates when currency matters.- The whole result set is buffered before anything is pushed. Value scoring is a percentile across the run, so every property is scraped and held in memory first; rows appear in the dataset only after the last one finishes. A run aborted mid-way writes nothing.
- Properties are fetched sequentially. No concurrency โ hotel pages go one after another, with a 1-second pause between search pages.
maxPropertiesaccepts up to 20000, but wall-clock time scales with it and the run timeout is the practical ceiling. - Filters apply to destination searches only.
stayCategory,starRating,minGuestRating,priceBandandsearchOrderare encoded into the search URL. Hotel detail URLs instayUrlsare fetched as given, unfiltered. minGuestRatingis truncated.8.5becomes8before conversion to Booking'sreview_score=80filter.- Relative dates are approximate.
1 monthresolves to 30 days from today,1 yearto 365. A date matching neither the absolute nor the relative form resolves to empty โ silently turning the run into a dateless scan. basePricefrom the legacy rooms table is a heuristic. On the DOM path the pre-discount price is inferred as the largest other plausible figure in the price cell (bounded to under 10ร the charged price), so an unrelated number printed there can distortbasePriceanddiscountPct. The Apollo path reads a named field and is unaffected.valueScoreis relative, not absolute. It ranks your result set against itself, not against all of Booking.com โ scrape 5 hotels and one is 100 and one is 0 by construction. RaisemaxPropertiesfor a meaningful spread.
How does this compare to other Booking.com scrapers?
| Feature | Booking.com Scraper โ Rate & Value Report | Generic Booking.com scraper |
|---|---|---|
| Output format | 40 typed top-level keys per property, identical every run; JSON, CSV, XLSX, XML, HTML | Raw rows, shape varies by Actor |
| Entity coverage | Property record + per-room rate options + derived value ranking in one item | Usually one entity โ either listings or reviews |
| Rate detail | basePrice, discountedPrice, discountPct, taxesAndFees, totalPrice, boardType, refundable per room option | A single nightly price field |
| Input flexibility | Destination search or hotel URLs or search-results URLs, plus 8 filters and base-Actor key aliases | Usually URL-only or destination-only |
| Anti-bot handling | Chrome-TLS fetch with a headless Chromium fallback, on a RESIDENTIAL โ DATACENTER โ NO_PROXY ladder with 3 retries per tier | Varies; some listings require you to bring your own proxies |
| Missing data policy | null / [], never a fabricated rate or an invented policy line | Varies |
The closest Apify Store listings were checked on 2026-07-25. plowdata/booking-com-review-scraper targets a different entity โ guest reviews and per-category scores across two datasets โ and its listing advertises "~30-50 reviews/sec" while instructing users to supply their own proxies because "Booking.com blocks shared Apify proxies" (its listing text, checked 2026-07-25 โ not measured here). ryanclinton/booking-scraper is the closest positional competitor: its listing advertises a stateful rate-intelligence engine with six modes, saved rate history across runs, and "up to 240 hotels per destination" (checked 2026-07-25 โ not measured here). This Actor is stateless by comparison, keeps no history between runs, and does one thing: a single-run value ranking with the published rate components underneath it.
If you are building an AI agent or a RAG pipeline, the output-format row is the decision-maker: parsing HTML inside an agent loop is a reliability failure mode, not a feature. Check the rate-detail row too โ a single price field cannot tell your model whether a discount is real or whether taxes are already included.
How many hotels can you scrape in one run?
maxProperties sets the ceiling: default 10, minimum 1, maximum 20000. There is no other hard cap inside the Actor โ no separate item limit, no internal page limit.
Pagination is offset-based: the Actor builds a Booking search URL, extracts every property-card link, then re-requests the same URL with &offset= incremented by 25 until it has maxProperties links or a page returns no new cards, pausing one second between pages. Detail pages are then fetched one at a time, each with up to 9 attempts across the proxy ladder. Any searchresults URL you pass in stayUrls is expanded the same way, and the combined list is truncated to maxProperties.
Two practical limits sit above the schema. Booking's own search paging depth bounds how many distinct properties one destination query can surface, so a large maxProperties on a small city stops early with "No more hotels found at offset N." And because the Actor is sequential and buffers the full result set before writing, the run timeout โ not maxProperties โ usually decides how large a run can be. No benchmark timing is quoted here because none has been measured.
What a run costs
Pricing is pay-per-event, declared in .actor/actor.json: $0.005 once per run for actor-start, and $0.01 per hotel row successfully pushed (row_result). Properties that fail every retry are skipped and never charged, and once your event limit is reached the Actor stops pushing and exits with "User spending limit reached". Rows that came back thin โ a property with no price, so valueScore: null โ are still pushed and still charged, because they are still a successfully scraped property record. Apify platform compute and proxy usage are billed separately by Apify under your plan.
๐ Integrate the Rate & Value Report into your workflow
The Actor runs on Apify, so it works with any language or tool that can send an HTTP request to the Apify API โ authentication is your Apify API token, and there is no separate account or key for the Actor itself.
REST API integration
from apify_client import ApifyClientclient = ApifyClient("<YOUR_APIFY_API_TOKEN>")run_input = {"location": "Amsterdam","maxProperties": 15,"sortResultsBy": "bestValue","stayCheckIn": "2026-09-12","stayCheckOut": "2026-09-15","resultCurrency": "EUR",}run = client.actor("<YOUR_USERNAME>/booking-scraper-rate-value-report").call(run_input=run_input)for item in client.dataset(run["defaultDatasetId"]).iterate_items():rate = (item.get("rateBreakdown") or [{}])[0]print(item["valueRank"], item["name"], item["price"], item["currency"],item["valueScore"], rate.get("boardType"), rate.get("discountPct"))
Works in Python, Node.js, Go, Ruby, cURL โ the same run-and-read-dataset pattern applies to every Apify API client.
Automation platforms (n8n, Make, LangChain)
n8n ships an official Apify node: choose the Run Actor operation, select this Actor, paste the JSON input, and pipe the dataset items onward โ a Postgres insert, a Slack digest of the top three valueRank rows, or a Google Sheets append.
Make has an Apify app with Run an Actor and Get Dataset Items modules. A common build is a scheduled scenario that runs the Actor each morning and filters to rows where rateBreakdown[].discountPct crosses a threshold before creating a record or firing an alert.
LangChain connects through the Apify integration: ApifyWrapper / ApifyDatasetLoader calls the Actor and maps each dataset item to a Document, so a value-ranked accommodation dataset can be embedded into a vector store and queried by a travel agent with no HTML in the pipeline.
Apify webhooks (fire on run success, POST the dataset id to your endpoint) and Apify schedules cover recurring monitoring.
Is it legal to scrape Booking.com?
Scraping publicly accessible Booking.com pages is generally lawful in most jurisdictions, and this Actor collects only such pages โ it does not log in, does not access member-only or loyalty-gated rates, does not bypass authentication, and never touches the booking or payment flow.
Most of what it returns is business and product data: listings, published rates, discounts, taxes, facilities and house rules. That category is governed by Booking.com's terms of service and, in the EU, by database rights โ review both before commercial redistribution and keep request volumes reasonable.
One part needs different handling. traderInfo, hostInfo and licenseInfo can contain a natural person's name, email, phone and postal address when the property is let by an individual rather than a company. That subset is personal data under GDPR and CCPA and requires a lawful basis to store or process. No reviewer identities and no guest data are collected. Consult legal counsel for commercial use cases involving bulk personal data.
โ Frequently asked questions
Does the Rate & Value Report work without a Booking.com account?
Yes. The Actor never logs in and holds no Booking.com credentials โ it fetches public search and hotel pages anonymously through Apify Proxy. The only credential involved anywhere is your Apify API token, and only if you start runs over the API rather than from the Console.
How often is the scraped data updated?
Every run is a live fetch. Nothing is cached between runs and no rate history is stored, so each row reflects the page as Booking served it at timeOfScrapeISO. For a time series, schedule the Actor and join successive datasets on source_url (or hotelId) plus checkInDate.
Why is rateBreakdown empty for some properties?
Almost always because the run had no stay dates. Booking only injects priced availability blocks for a specific check-in/check-out pair, so a dateless run returns rateBreakdown: [] by design โ the Actor logs a warning rather than inventing a rate. It is also empty when includeRateBreakdown is false, when the property is sold out for your dates, or when Booking served a layout with neither Apollo room blocks nor the legacy rooms table. price can still be populated from Booking's inline cheapest-price value.
What happens when a property is delisted or a page returns nothing?
The property is retried through the full proxy ladder โ up to 9 attempts across RESIDENTIAL, DATACENTER and NO_PROXY โ and if no hotel name can be extracted, the Actor logs Giving up on <url> and skips it. No partial row is pushed and no row_result event is charged. If nothing could be extracted at all, the run finishes cleanly with "Nothing extracted from any hotel URL" and an empty dataset, so downstream jobs see zero rows rather than malformed ones.
Can I scrape login-gated or member-only Booking.com rates?
No. Only publicly accessible content is returned. Genius and other loyalty-gated prices, anything behind a Booking.com login, guest or reviewer personal details, and the booking/checkout flow are all outside what this Actor reads. If a rate is only visible to a signed-in member, it will not appear in rateBreakdown.
How do I try it before committing to a large run?
Start it from its Apify Store listing with the prefilled input โ location: "Amsterdam", maxProperties: 10 โ and add your own stay dates. Under pay-per-event a small run costs the actor-start event plus one row_result per property returned, so a 10-property test is easy to price before scaling maxProperties up.
Does it work for AI agent workflows and LLM pipelines?
Yes. The Actor is callable as an HTTP endpoint by any agent framework through the Apify API โ LangChain, LlamaIndex, a custom tool-calling loop, or an n8n/Make automation. Every response is typed JSON with stable keys, so nothing needs parsing between the scrape and the model's context window. valueScore, rateBreakdown[].boardType and rateBreakdown[].refundable map directly onto the constraints a travel agent is asked to satisfy.
How does the Actor handle Booking.com's anti-bot system?
With three layers, all in the source. Requests go out through an impit client that impersonates Chrome's TLS fingerprint, over an Apify Residential proxy by default. A response that looks blocked โ under 3000 characters, or containing captcha, robot check, are you a robot, px-captcha or access denied โ triggers a headless Chromium fallback with a real browser context, Chrome user agent, 1366ร900 viewport and automation flags disabled. Repeated failures walk a proxy ladder: RESIDENTIAL, then DATACENTER, then no proxy, 3 retries per tier. Pin a proxy country with apifyProxyCountry in proxyConfiguration.
Does it return data in a format LLMs can use directly?
Yes. Typed, normalized JSON with stable field names and consistent key ordering on every row โ no HTML, no selectors, no post-processing. Pass the dataset straight into an LLM context window, index it into a vector store, or expose it as an agent tool. Absent values are explicit null / [], which is what a model needs to reason about missing data instead of hallucinating around a blank string.
Can I use it without managing proxies?
Yes. proxyConfiguration defaults to Apify Proxy with the RESIDENTIAL group, and the fallback ladder, retry counts and browser fallback are handled inside the Actor. You do not supply proxy credentials, rotate sessions, or configure a browser. Change it only for a specific reason โ a country requirement, or your own proxy setup.
What happens when Booking.com changes its page structure?
The output schema stays put. The extractor reads four independent layers of every page โ Apollo store, JSON-LD, inline window.booking variables, DOM selectors โ and merges what each yields, so a markup change usually costs one field rather than the run. Key names and types do not change on your side, so warehouse tables and agent tools keep working while the scraper is updated behind them.
๐ฌ Your feedback
Found a bug, hit a Booking.com layout the extractor does not cover, or need a field that is on the page but not in the schema? We want to know. Open an issue on the Actor's Issues tab on its Apify Store listing โ include your input JSON and the run id, and the parsing path can be traced from the run log. Feature requests for new filters or extra rate components are welcome there too.