kestrel actors docs

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

EventWhat it isPriceCharged for
hotelPriced hotel row$0.004One 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.
roomRoom rate row$0.002One 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.

FieldTypeDefaultWhat it does
locationQueriesarrayPlaces 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.
maxHotelsPerQueryinteger50How 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.
startUrlsarrayBooking.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).
hotelNamesarrayProperty 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.
hotelIdsarrayBooking'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.
checkInstringYYYY-MM-DD, or relative so a schedule never goes stale: "30 days", "6 weeks", "tomorrow". Check-out is checkIn + nights.
nightsinteger1Length of stay. price_total is for the whole stay; price_nightly is the total divided by this. Range 1-30.
adultsinteger2Adults across all rooms. Booking prices the occupancy you ask for, so a solo traveller and a couple can see different rates. Range 1-30.
childrenAgesarrayOne age (0-17) per child, e.g. 5 and 9. Booking needs ages, not a count, to match family rooms and child policies.
roomsinteger1How many rooms the guests need. With two rooms Booking returns two room blocks per property and the total covers both. Range 1-30.
currencystring"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.
sortBystring (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.
minStarsinteger00 = 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.
maxPricenumber00 = 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.
includeRoomsbooleanfalseAlso 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.
localestring"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.
sessionsinteger4How many proxy sessions (egress IPs) run in parallel. More is faster; each is paced separately. Range 1-16.
perIpnumber1Pace 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.
proxyConfigurationobject{"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.

FieldTypeWhat it holds
typestringhotel (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_idstringBooking's numeric property id
ufiintegerBooking's city/area id (negative numbers are cities)
namestring
urlstringThe Booking.com property page
property_typestringhotel, 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_idintegerBooking's accommodation type id
starsnumberStar rating as shown by Booking (null when the property has none)
review_scorenumberBooking guest score, 1-10
review_countinteger
addressstring
citystring
country_codestringISO 3166-1 alpha-2, lower case
latitudenumber
longitudenumber
display_locationstringBooking's location line, e.g. 'Alfama, Lisbon'
distance_from_centerstringe.g. '0.8 km from downtown'
transportstringe.g. 'Terreiro do Paço station is within 500 meters'
check_instringYYYY-MM-DD
check_outstringYYYY-MM-DD
nightsinteger
adultsinteger
childreninteger
roomsintegerRooms requested
currencystringThe currency of price_total and price_nightly (your currency input)
price_totalnumberThe lowest price Booking shows for the whole stay and party, in currency (null = sold out / unpriced)
price_nightlynumberprice_total ÷ nights
price_displaystringBooking's formatted total, e.g. 'US$482.38'
price_hotel_currency_totalnumberThe same stay total in the property's own currency
hotel_currencystring
price_before_discountnumberThe crossed-out total when a deal applies (0 or null when none)
taxes_fees_excludednumberCharges Booking lists as excluded from price_total, in currency (null when everything is included)
taxes_includedbooleanTrue when Booking lists no excluded charges for the stay
charges_notestringBooking's own wording, e.g. '+US$9 taxes and fees' or 'Includes taxes and fees'
free_cancellationbooleanBooking's free-cancellation badge for the cheapest option
no_prepaymentboolean
meal_planstringe.g. 'Breakfast included'
breakfast_includedboolean
room_namestringThe room behind the headline price
rooms_listedintegerHow many room and rate combinations Booking matched to the stay
sold_outbooleanTrue when Booking shows no price for these dates
sold_out_messagestring
alternative_check_instringBooking's suggested dates when the stay is unavailable
alternative_check_outstring
sponsoredbooleanA paid placement in the results
newly_openedboolean
sustainableboolean
host_typestringPROFESSIONAL or PRIVATE, when Booking states it
imagestring
querystringThe destination or hotel target this row came from
rankintegerPosition in the destination results under sortBy (1 = first)
hotel_namestringOn room and status rows
room_idstring
occupancyintegerGuests the block is priced for
meal_plan_idinteger
policy_group_idstring
price_originalnumberThe block price in the hotel currency
original_currencystring
free_cancellation_untilstringISO timestamp
only_x_leftstringe.g. 'Only 2 left at this price on our site'
has_cribboolean
third_partyboolean
targetstringThe destination or hotel target as given
kindstringdestination or hotel
dest_idinteger
dest_typestringCITY, REGION, DISTRICT, LANDMARK, AIRPORT, COUNTRY or HOTEL
statusstringok, no_results, not_found, duplicate or error
hotelsinteger
pricedintegerCharged rows
unpricedintegerDelivered free
filteredintegerDropped by maxPrice before billing
duplicatesintegerProperties already delivered by another target
pagesinteger
totalintegerBooking's result count for the destination
errorstring
fetched_atstringISO 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 proxyConfiguration is set accordingly; changing it to datacenter makes every search fail.
  • "Top picks" shifts between pages. Booking's default popularity order is personalised and reshuffles, so a long paged run under it can repeat properties (deduplicated, counted in duplicates) and miss others. Use price, review_score, stars or distance for 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_nightly is 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_excluded and charges_note say which, per row.
  • Sold-out properties still appear (free) with sold_out: true and Booking's alternative dates when it offers them. With maxPrice set 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 an error row; 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:

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

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