kestrel actors › Hotel & flight prices
Booking.com Scraper — Hotel Prices & Availability by Date
This Booking.com scraper is a hotel price scraper for exact dates, guests and rooms: it returns Booking.com hotel prices for every property in a destination, or for a list of hotels by URL, name or id, each as one clean row with the stay total in your currency, the nightly price, the taxes and fees Booking leaves out, free-cancellation and breakfast flags, stars, review score, location and the cheapest room. It reads the same call Booking.com's own search results page makes, so there is no browser, no login and no Booking.com API key — a way to scrape Booking.com without API access — and you pay per priced property, never for sold-out ones.
Run Booking.com Scraper on Apify →
Booking.com scraper pricing: what one row costs
| Event | What it is | Price | Charged for |
|---|---|---|---|
hotel | Priced hotel row | $0.004 | One property with a price for your dates and guests: stay total, nightly rate, taxes and fees, cancellation and breakfast flags, stars, review score, location and the cheapest room. Sold-out and unpriced properties are free, and so are status rows. |
room | Room rate row | $0.002 | One room and rate combination Booking.com matched to the stay (only with includeRooms): room name, occupancy, meal plan, stay total, hotel-currency price, free-cancellation deadline and scarcity message. |
$0.004 per priced hotel row and $0.002 per room row, plus nothing else: sold-out and unpriced properties, status rows, filtered rows and refused runs are free. Pricing 100 Lisbon properties is $0.40; a daily comp set of 10 hotels with room rates (say 60 rooms) is $0.16 a day. Apify's platform usage for a 100-property search is well under a cent.
Booking.com scraper input reference
Every field of Booking.com Prices, straight from the actor's input schema.
| Field | Type | Default | What it does |
|---|---|---|---|
locationQueries | array | — | Places as you would type them into Booking.com's search box — a city, region, district, landmark, airport or country ("Lisbon", "Algarve", "Times Square", "Portugal"). Each becomes one search for your dates, paged up to maxHotelsPerQuery properties. Leave empty to price only the hotels below. |
maxHotelsPerQuery | integer | 50 | How many properties to take from each destination search, in the order sortBy gives (Booking pages 100 at a time). The main cost control for destination runs. Range 1-1000. |
startUrls | array | — | Booking.com property pages, e.g. https://www.booking.com/hotel/pt/memmo-alfama.html — each is priced on its own for your dates. Resolved through Booking's autocomplete (a URL carries no id). |
hotelNames | array | — | Property names as you would type them into Booking's search box, e.g. "Memmo Alfama Lisbon". The first matching property wins; check the status row's hotel_name. |
hotelIds | array | — | Booking's numeric property ids, e.g. 536251 — what an earlier run's hotel_id column holds. The fastest way to re-price a portfolio or a comp set every day. |
checkIn | string | — | YYYY-MM-DD, or relative so a schedule never goes stale: "30 days", "6 weeks", "tomorrow". Check-out is checkIn + nights. |
nights | integer | 1 | Length of stay. price_total is for the whole stay; price_nightly is the total divided by this. Range 1-30. |
adults | integer | 2 | Adults across all rooms. Booking prices the occupancy you ask for, so a solo traveller and a couple can see different rates. Range 1-30. |
childrenAges | array | — | One age (0-17) per child, e.g. 5 and 9. Booking needs ages, not a count, to match family rooms and child policies. |
rooms | integer | 1 | How many rooms the guests need. With two rooms Booking returns two room blocks per property and the total covers both. Range 1-30. |
currency | string | "USD" | ISO code for every price (USD, EUR, GBP, …). The hotel's own currency is kept alongside in hotel_currency and price_hotel_currency_total. |
sortBy | string (enum) | "popularity" | Booking's own sort orders. "Top picks" is personalised and shifts between pages; the price, review and star orders are stable and are what a price monitor should use. One of: popularity, price, price_desc, review_score, review_score_and_price, stars, stars_asc, stars_and_price, distance, homes_first. |
minStars | integer | 0 | 0 = every property. 1-5 = Booking's own star filter, applied by Booking before the results are fetched — filtered properties are never fetched or billed. Range 0-5. |
maxPrice | number | 0 | 0 = every priced property. N = keep only properties whose nightly rate (stay total ÷ nights, in your currency) is N or less. Filtered properties are never delivered or billed; with this set, sold-out properties are dropped too. Minimum 0. |
includeRooms | boolean | false | Also deliver one room row per room and rate combination Booking matched to the stay (name, occupancy, meal plan, price, free-cancellation deadline, "only X left"). Billed per room row. |
locale | string | "en-US" | Booking locale for the request, e.g. en-US, en-GB, de-DE, fr-FR. It sets the language of labels such as the meal plan and distance texts and the number format in price_display. |
sessions | integer | 4 | How many proxy sessions (egress IPs) run in parallel. More is faster; each is paced separately. Range 1-16. |
perIp | number | 1 | Pace for each session. Booking answered 10 searches at 1/s from one IP with no refusals; a search takes 2-4 s anyway. Range 0.1-3. |
proxyConfiguration | object | {"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]} | Apify Proxy with the RESIDENTIAL group is required: Booking's search call answers datacenter IPs with its WAF challenge, while residential IPs are served without cookies or tokens. Keep the default. |
Example input
{
"locationQueries": ["Lisbon"],
"maxHotelsPerQuery": 100,
"checkIn": "6 weeks",
"nights": 2,
"adults": 2,
"currency": "EUR",
"sortBy": "price"
}
{
"hotelIds": ["536251", "2251985", "1471925"],
"checkIn": "30 days",
"nights": 1,
"adults": 2,
"currency": "USD",
"includeRooms": true
}
Booking.com scraper output fields
One row per property with its price for the stay, optional room-rate rows, and one status row per target.
Dataset views in the Apify Console: Overview, Hotels, Cheapest first, Room rates, Status.
| Field | Type | What it holds |
|---|---|---|
type | string | hotel (a property with its price for the stay), room (one room and rate combination, only with includeRooms) or status (one per destination or property) |
hotel_id | string | Booking's numeric property id |
ufi | integer | Booking's city/area id (negative numbers are cities) |
name | string | |
url | string | The Booking.com property page |
property_type | string | hotel, apartment, hostel, bed_and_breakfast, guest_house, holiday_home, villa, resort, homestay, motel, aparthotel, campsite, lodge, capsule_hotel, inn — or null for rarer types (see property_type_id) |
property_type_id | integer | Booking's accommodation type id |
stars | number | Star rating as shown by Booking (null when the property has none) |
review_score | number | Booking guest score, 1-10 |
review_count | integer | |
address | string | |
city | string | |
country_code | string | ISO 3166-1 alpha-2, lower case |
latitude | number | |
longitude | number | |
display_location | string | Booking's location line, e.g. 'Alfama, Lisbon' |
distance_from_center | string | e.g. '0.8 km from downtown' |
transport | string | e.g. 'Terreiro do Paço station is within 500 meters' |
check_in | string | YYYY-MM-DD |
check_out | string | YYYY-MM-DD |
nights | integer | |
adults | integer | |
children | integer | |
rooms | integer | Rooms requested |
currency | string | The currency of price_total and price_nightly (your currency input) |
price_total | number | The lowest price Booking shows for the whole stay and party, in currency (null = sold out / unpriced) |
price_nightly | number | price_total ÷ nights |
price_display | string | Booking's formatted total, e.g. 'US$482.38' |
price_hotel_currency_total | number | The same stay total in the property's own currency |
hotel_currency | string | |
price_before_discount | number | The crossed-out total when a deal applies (0 or null when none) |
taxes_fees_excluded | number | Charges Booking lists as excluded from price_total, in currency (null when everything is included) |
taxes_included | boolean | True when Booking lists no excluded charges for the stay |
charges_note | string | Booking's own wording, e.g. '+US$9 taxes and fees' or 'Includes taxes and fees' |
free_cancellation | boolean | Booking's free-cancellation badge for the cheapest option |
no_prepayment | boolean | |
meal_plan | string | e.g. 'Breakfast included' |
breakfast_included | boolean | |
room_name | string | The room behind the headline price |
rooms_listed | integer | How many room and rate combinations Booking matched to the stay |
sold_out | boolean | True when Booking shows no price for these dates |
sold_out_message | string | |
alternative_check_in | string | Booking's suggested dates when the stay is unavailable |
alternative_check_out | string | |
sponsored | boolean | A paid placement in the results |
newly_opened | boolean | |
sustainable | boolean | |
host_type | string | PROFESSIONAL or PRIVATE, when Booking states it |
image | string | |
query | string | The destination or hotel target this row came from |
rank | integer | Position in the destination results under sortBy (1 = first) |
hotel_name | string | On room and status rows |
room_id | string | |
occupancy | integer | Guests the block is priced for |
meal_plan_id | integer | |
policy_group_id | string | |
price_original | number | The block price in the hotel currency |
original_currency | string | |
free_cancellation_until | string | ISO timestamp |
only_x_left | string | e.g. 'Only 2 left at this price on our site' |
has_crib | boolean | |
third_party | boolean | |
target | string | The destination or hotel target as given |
kind | string | destination or hotel |
dest_id | integer | |
dest_type | string | CITY, REGION, DISTRICT, LANDMARK, AIRPORT, COUNTRY or HOTEL |
status | string | ok, no_results, not_found, duplicate or error |
hotels | integer | |
priced | integer | Charged rows |
unpriced | integer | Delivered free |
filtered | integer | Dropped by maxPrice before billing |
duplicates | integer | Properties already delivered by another target |
pages | integer | |
total | integer | Booking's result count for the destination |
error | string | |
fetched_at | string | ISO timestamp |
Example row from a real run
Taken from the actor's README — this is the shape your dataset holds.
{
"type": "hotel",
"hotel_id": "536251",
"name": "Memmo Alfama - Design Hotels",
"url": "https://www.booking.com/hotel/pt/memmo-alfama.html",
"property_type": "hotel",
"stars": 4,
"review_score": 9.3,
"review_count": 894,
"city": "Lisboa",
"country_code": "pt",
"display_location": "Santa Maria Maior, Lisbon",
"distance_from_center": "0.8 km from downtown",
"check_in": "2026-09-28",
"check_out": "2026-09-29",
"nights": 1,
"adults": 2,
"currency": "EUR",
"price_total": 415.09,
"price_nightly": 415.09,
"price_display": "€ 415.09",
"hotel_currency": "EUR",
"taxes_included": true,
"free_cancellation": true,
"breakfast_included": true,
"room_name": "Superior Double Room with Terrace",
"rooms_listed": 3,
"sold_out": false,
"rank": 1,
"query": "Memmo Alfama Lisbon"
}
Booking.com scraper in Python, JavaScript and curl
Python
from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("kestrel/booking-prices-scraper").call(run_input={
"locationQueries": ["Lisbon"], "maxHotelsPerQuery": 50,
"checkIn": "30 days", "nights": 1, "adults": 2, "currency": "EUR", "sortBy": "price",
})
rows = [r for r in client.dataset(run["defaultDatasetId"]).iterate_items() if r["type"] == "hotel" and r["price_total"] is not None]
for r in rows[:10]:
print(r["rank"], r["name"], r["stars"], r["review_score"], r["price_nightly"], r["currency"], r["room_name"])
JavaScript (Node)
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('kestrel/booking-prices-scraper').call({
hotelIds: ['536251', '2251985'], checkIn: '30 days', nights: 1, adults: 2, currency: 'USD', includeRooms: true,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
const hotels = items.filter(r => r.type === 'hotel');
console.log(hotels.map(h => [h.name, h.price_nightly, h.free_cancellation]));
curl
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 '{"locationQueries": ["Algarve"], "maxHotelsPerQuery": 30, "checkIn": "6 weeks", "nights": 3, "adults": 2, "currency": "GBP", "sortBy": "review_score", "minStars": 4}'
The same three calls are runnable files in the examples repository: examples/booking-prices-scraper.py|.js|.sh.
Limits and honest notes
- Residential proxy required. Booking's search call answers datacenter addresses with its WAF challenge but serves residential ones without cookies or tokens. The default
proxyConfigurationis set accordingly; changing it to datacenter makes every search fail. - "Top picks" shifts between pages. Booking's default
popularityorder is personalised and reshuffles, so a long paged run under it can repeat properties (deduplicated, counted induplicates) and miss others. Useprice,review_score,starsordistancefor a complete, repeatable list. - Prices are for your party. The stay total depends on adults, children's ages and rooms; a solo traveller sees different rates than a couple.
price_nightlyis the total divided by nights, not Booking's own per-night figure (which it omits for most searches). - Taxes vary by market. In some countries the total includes taxes; elsewhere Booking lists them as excluded.
taxes_included,taxes_fees_excludedandcharges_notesay which, per row. - Sold-out properties still appear (free) with
sold_out: trueand Booking's alternative dates when it offers them. WithmaxPriceset they are dropped, since they have no price to compare. - Room rows describe the combinations Booking matched to the party, not the property's whole inventory; a room can appear twice with different meal plans or cancellation policies.
- A destination that names a property ("Memmo Alfama Lisbon") prices just that property — the status row says
kind: hotel. - Unknown ids are reported as
not_found, free. An unplaceable destination is anerrorrow; the run continues with the other targets.
FAQ
Does it need a Booking.com API key or login?
No. It uses the same GraphQL call Booking's public search results page makes, anonymously, and only needs Apify's residential proxy group.
Can I get Booking.com hotel prices for exact dates?
Yes — that is the whole point. checkIn plus nights sets the stay; adults, childrenAges and rooms set the party; currency sets the money. Every row carries those values.
How is this different from a browser-based Booking.com scraper?
It never renders a page. A 100-property search is one request that takes about three seconds, so a market scan costs a few cents of platform time instead of minutes of browser time, and it does not break when Booking changes its page layout.
Which sort orders are repeatable?
price, price_desc, review_score, stars, stars_asc, distance and their combinations are deterministic. popularity is Booking's personalised "top picks" and moves between pages.
Can I filter by star rating or price before paying?
Yes. minStars is sent to Booking as its own filter, so those properties are never fetched; maxPrice drops rows above your nightly cap before they are billed.
Does it return room-level rates?
With includeRooms: true you get one room row per room and rate combination, billed at $0.002 each. Without it you still get the cheapest room's name on the hotel row.
What does taxes_included: false mean?
Booking lists charges it excludes from the displayed total (city tax, VAT, resort fees). taxes_fees_excluded holds their amount in your currency and charges_note Booking's wording.
Why does the same hotel come back at a slightly different price on the next run?
Booking.com prices a stay per session. Two runs seconds apart can differ by a few per cent to a quarter — the site rotates promotional rates, mobile-only deals and currency handling per session, and price_before_discount is present on some answers and absent on others. That is the site's behaviour, not a parsing artefact, and every scraper of it sees the same thing.
Three ways to work with it rather than against it:
- Compare the same field across runs (
price_totalagainstprice_total), never a nightly rate from one run with a stay total from another. - Give a price-drop alert a threshold — three per cent filters the noise, and the n8n price-drop template ships with exactly that knob.
- For a decision that must be defensible (a rate-parity report, a revenue-management brief), take the median of a few runs rather than a single reading, or cross-check the stay against Google Hotels Prices Scraper, which reports what Google's own comparison shows for the same dates.
Can I use it for hotel price comparison across sites?
Yes — pair it with the Google Hotels and rate-parity actors below, which return every booking site's rate for the same stay, and compare the price_total columns.
How do I build a Booking.com price history?
Schedule a run with a relative checkIn and a deterministic sortBy; each day's rows share the same lead time and columns, so appending them to a sheet or a table is a price history.
Can I scrape Booking.com without an API?
That is what this does: no Booking.com API, no key, no browser — a plain request through Apify's proxy. If you have been searching for a "Booking.com API" for prices, this is the practical answer: the same data the site shows, as rows.
What does bulk work cost?
1,000 properties across ten destinations is 1,000 × $0.004 = $4.00 plus a cent or two of platform usage; ten requests of 100 rows each.
How to use Booking.com Scraper
Related scrapers
Run Booking.com Scraper
The actor lives on the Apify Store, where the pricing tab carries the live per-row price and the input form has every field above: apify.com/kestrel/booking-prices-scraper