Booking.com Hotel Scraper – Prices, Room Rates & Reviews avatar

Booking.com Hotel Scraper – Prices, Room Rates & Reviews

Pricing

from $1.00 / 1,000 search results

Go to Apify Store
Booking.com Hotel Scraper – Prices, Room Rates & Reviews

Booking.com Hotel Scraper – Prices, Room Rates & Reviews

Scrape Booking.com hotel prices, room rates and availability via the mobile API. Full per-room rate table with itemised taxes and fees, family pricing by child age, guest reviews and property details — one actor, not four. Search by destination, GPS radius or hotel id. No API key needed.

Pricing

from $1.00 / 1,000 search results

Rating

0.0

(0)

Developer

R.L.

R.L.

Maintained by Community

Actor stats

0

Bookmarked

150

Total users

90

Monthly active users

19 hours

Issues response

a day ago

Last modified

Categories

Share

Booking.com API Scraper – Hotel Prices, Rooms, Reviews & Availability

Scrape Booking.com at scale — extract hotels, live room prices, availability, full rate breakdowns, facilities, photos and guest reviews as clean, structured JSON. No login, no captchas, no browser automation to maintain. Just give it a destination and dates, and get back ready-to-use accommodation data you can export to CSV, Excel, JSON or the API.

Whether you're tracking competitor hotel prices, building a travel app, feeding a rate-shopping dashboard, or doing market research, this Booking.com data extractor turns the world's largest accommodation site into a structured feed.

Fast and reliable — calls Booking.com's own mobile app API (mobile-apps.booking.com) directly instead of rendering pages, so it runs without browser automation or site login, and is dramatically faster and more stable than page-rendering scrapers. No partner contract needed — this Actor does not call the partner-gated Demand API and needs no affiliate ID, API key or partner approval. It reads Booking.com's public mobile app API and returns clean, search-style JSON.

What you can extract

  • Hotel search results – every property for a city, region, free-text destination or GPS radius, with live total price, currency, and direct booking links.
  • Live prices and itemised taxes – per-stay pricing with every tax and fee broken out (city tax, VAT, service charge, tourism fee, cleaning fee…), in the currency you choose.
  • Family stays priced by child age – pass the children's ages and get the rates Booking would actually sell to your party, with family rooms surfacing and non-fitting rooms flagged.
  • Rooms and availability – the full list of room types and bookable rates: occupancy, structured bed layout (sofa bed? how many beds?), bedrooms and bathrooms, size, board, cancellation policy, payment options, and how many rooms are left at each price.
  • Availability status – for a fixed list of hotels you always get a record back, and it says why there is no price: sold out, minimum stay not met (with the required nights), closed, or unknown id.
  • Guest reviews – paginated reviews with score, title, positive/negative text, date and reviewer country (fetch as many as you need per property).
  • Property details – name, star rating, review score & count, GPS coordinates, address, 100+ named facilities, family amenities, languages spoken, house rules and photos.

Example use cases

  • Hotel price monitoring & rate shopping – track competitor and market prices over time for revenue management.
  • Family & group travel products – price stays correctly for parties with children, and only show rooms that fit.
  • Travel & metasearch products – power a booking site, app or price-comparison tool with fresh inventory.
  • Market & investment research – analyze pricing, occupancy signals, ratings and amenities across a destination.
  • Review & reputation analysis – mine guest sentiment and review trends for any property.
  • Lead lists & data enrichment – build accommodation datasets with geo, facilities and contact context.

Why this scraper, not a flat-listing one

Most Booking.com scrapers return one row per hotel with a single total price. That is enough to see who is cheapest today; it is not enough to do revenue management, price a family stay, or explain a number to a client. This actor returns the layer underneath:

  • It returns the full per-stay price breakdown with every tax and fee itemised — city tax, VAT, service charge, tourism fee, cleaning fee — not one blended total.
  • It prices family stays correctly by child age. Pass childrenAges and you get the rates Booking would actually sell to your party. A scraper that takes only a child count cannot do this, because Booking's pricing depends on the ages.
  • It returns every bookable room option per property, with occupancy, structured bed layout, board, cancellation policy, payment terms and how many rooms are left at each price.
  • It tells you why a hotel has no price. Query a fixed list of hotel ids and you always get a record back, saying sold out, minimum stay not met (with the nights required), closed, or unknown id — instead of the property silently vanishing.
  • It searches by GPS radius or bounding box, not just by destination name.
  • It is one actor, not four. Search, property details, rooms and reviews come from the same run, so you do not chain a listing scraper into a separate reviews scraper and a separate photos scraper.

Compared with the alternatives

Prices are the vendors' own published per-event rates at the time of writing (BRONZE tier); check each Store page for current numbers.

This actorTypical flat-listing scraperReviews-only scrapers
Rows per hotelsearch row + details + every room rateone flat rown/a
Taxes & feeseach one itemisedblended total, or absentn/a
Family pricingby child age (childrenAges)child count at bestn/a
Why no priceexplicit status (sold out / min stay / closed / unknown id)hotel just missingn/a
Search by GPS radiusyesrarelyn/a
Reviewssame run, $0.0001 eachseparate actor$0.0012 – $0.04 each
Per search result$0.001$0.003 – $0.015n/a
Per-run start feenone$0.00005 – $0.10$0.00005 – $0.01

The per-run start fee matters more than it looks on a schedule: a fee charged per run is charged whether the run returns 2 rows or 2,000, so it falls hardest on exactly the small, frequent runs that price monitoring is made of.

How to use it

No coding required. Set up your search in the visual input form, then run it on demand, schedule it, or call it via the API and connect it to Make, Zapier, n8n, Google Sheets and more. Results land in a dataset you can download as JSON, CSV, Excel or HTML, or pull through the API.

Children / family stays

Child ages are supported — and required for correct family prices. Booking.com prices a stay by the age of each child (free cots, kids-stay-free rules, extra beds), so a party of 2 adults + children aged 2 and 9 pays a different price than 2 adults + teenagers, and a different one again than 4 adults. Give the actor the ages and it sends them on every request — the destination search, the hotel page and the room list — and every price you get back is for exactly your party.

Two equivalent ways to say it; the flat fields win when both are present:

{ "adults": 2, "rooms": 1, "childrenAges": [5, 9] }
{ "guests": { "number_of_adults": 2, "number_of_rooms": 1, "children": [5, 9] } }

Per-room guests.allocation[] ([{ "number_of_adults": 2, "children": [5] }, …]) is accepted too; the party is summed because the mobile API takes aggregate occupancy.

With children in the party:

  • family rooms surface, and rooms that can't host the party are marked fitsParty: false (and, by default, left out of products),
  • each product carries occupancy.requestedChildrenAges so you can confirm the ages reached Booking,
  • the web url of every result includes group_children / age parameters, so clicking through lands on the same party.

Verified on Hotel Hortus, Amsterdam (2026-12-10 → 12, EUR): 2 adults + children (2, 9) = 452.80, 2 adults + children (12, 15) = 603.73, 4 adults = 603.73 — the same 0.75 ratio the booking.com website shows.

Ready-made examples (one click, no setup)

Each of these is a published task: open it, press run, and it works with the input already filled in. Running one copies it into your own account, so you can change anything afterwards without affecting the original.

ExampleWhat it does
Rate shop your competitive set dailyTracks a fixed compset by property ID, room by room, taxes itemised
Monitor hotel prices in AmsterdamCity-wide price monitoring with taxes and fees
Monitor hotel availability and sell-outsFlags sold-out properties instead of dropping them, for spotting compression dates
Get full room rate tables with taxes in TokyoEvery bookable rate per property, with board, cancellation terms and taxes
Collect guest reviews and reputation scoresReviews plus per-category subscores: cleanliness, staff, location, value
Find the cheapest well-rated hotels in Barcelona8.0+ rated properties, cheapest first
Price family rooms in Rome by children's agesReal family pricing, keeping only rates that fit the party
Scrape apartments and holiday homes in LisbonSelf-catering supply with bedroom and bathroom counts
Find hotels within walking distance of a landmarkCoordinate search with a radius, nearest first

None of them pins a date: check-in defaults to 30 days ahead, so every example returns live availability rather than going stale.

Which example matches my job?

I want to…ExampleKey input fields
price a stay for a family and see which rooms fit1. Family stay with childrensearch, childrenAges, includeRooms
watch prices for a fixed list of hotels2. Price-monitor a fixed list of propertieshotel_ids, currency, includeExtraCharges
find the cheapest well-rated hotels in a city3. Cheapest well-rated hotels in a citysearch, minReviewScore, maxPrice, sortBy
only get apartments / holiday homes4. Apartments and holiday homes onlyaccommodationTypes
pull guest reviews for competitor analysis5. Guest reviewshotel_ids, includeReviews, maxReviews
search around a GPS point6. Radius search around coordinateslatitude, longitude, radius
enrich a property with everything static7. Full property detailsincludeDetails
know why a hotel has no price8. Sold out or minimum stay?hotel_ids (always emits a record)

Use cases

Every input below validates against the actor's input schema; every output is a trimmed record from a real run on 2026-09-17 (fields not relevant to the scenario are replaced with …). The complete record shape is on the Output tab of the Store page.

1. Family stay with children

A family of two adults with a 5- and a 9-year-old wants a two-night stay in Amsterdam and needs to know what it will actually cost them, per room type. Passing childrenAges makes Booking price the party by age; includeRooms adds the room list, and by default (onlyFittingProducts: true) products only holds rates Booking would sell to this party. rooms still lists every room type, so you can see which ones do not fit.

{
"search": "Amsterdam",
"checkin": "2026-12-10",
"checkout": "2026-12-12",
"currency": "EUR",
"adults": 2,
"childrenAges": [5, 9],
"includeRooms": true,
"maxItems": 3
}
{
"id": 10422,
"name": "ibis Amsterdam Centre",
"url": "https://www.booking.com/hotel/nl/ibiscentre.html?…&group_adults=2&group_children=2&age=5&age=9",
"price": { "total": 599.17, "extra_charges": { "excluded": 200.72, "…": "…" } },
"rooms": [
{ "name": "Family Room", "maxOccupancy": 4, "fitsParty": true,
"productCount": 4, "minPrice": 799.9, "minPriceFitting": 799.9,
"bedCount": 3, "hasSofaBed": false,
"beds": [ { "type": "double", "count": 1, "label": "1 full bed" },
{ "type": "twin", "count": 2, "label": "2 twin beds" } ],
"childrenPolicy": { "maxChildrenFree": 0, "maxChildrenFreeAge": 3 } },
{ "name": "Standard Double Room", "maxOccupancy": 2, "fitsParty": false,
"productCount": 0, "minPrice": null, "minPriceFitting": null, "…": "…" }
],
"products": [
{ "id": "1042225_95453819_4_2_0", "room": 1042225, "roomName": "Family Room",
"fitsParty": true, "maxOccupancy": 4,
"occupancy": { "adults": 4, "children": 0,
"requestedAdults": 2, "requestedChildrenAges": [5, 9] },
"price": { "base": 599.17, "total": 799.9,
"extra_charges": { "included": [
{ "charge": "CITYTAX", "name": "City tax", "mode": "percentage", "percentage": 12.5, "total_amount": 74.9 },
{ "charge": "VAT", "name": "VAT", "mode": "percentage", "percentage": 21, "total_amount": 125.83 } ] } },
"policies": { "cancellation": { "type": "free_cancellation", "…": "…" }, "…": "…" } }
]
}

occupancy.adults / children is how Booking allocates the party to this rate (a 9-year-old may count as an adult-equivalent, a 2-year-old as a child in an existing bed); requestedAdults / requestedChildrenAges echo what you asked for. The search-path price.total (599.17) is Booking's display price before the listed taxes; price.total + extra_charges.excluded = the room's gross 799.9.

2. Price-monitor a fixed list of properties

You know the Booking ids of the properties you compete with and want their headline price for a date, with taxes itemised, every morning. hotel_ids skips the search and fetches each property directly; the top-level price is the cheapest rate that fits the party (2 adults by default), and extra_charges lists every tax and fee behind it.

{
"hotel_ids": ["1392565", "235613", "10422"],
"checkin": "2026-12-10",
"checkout": "2026-12-12",
"currency": "EUR",
"adults": 2,
"includeExtraCharges": true
}
{
"id": 1392565,
"name": "Mercure Amsterdam Sloterdijk Station",
"currency": "EUR",
"checkin": "2026-12-10", "checkout": "2026-12-12", "datesAdjusted": false,
"price": {
"base": 191.74, "book": 275.97, "total": 275.97,
"extra_charges": {
"included": 84.23, "excluded": 0,
"details": [
{ "charge": "VAT", "name": "VAT", "mode": "percentage", "percentage": 21, "total_amount": 40.26, "currency": "EUR", "inclusion": "included" },
{ "charge": "CITYTAX", "name": "City tax", "mode": "percentage", "percentage": 12.5, "total_amount": 23.97, "currency": "EUR", "inclusion": "included" },
{ "charge": "SERVICECHARGE", "name": "Service charge", "mode": "per_night", "unit_amount": 10, "total_amount": 20, "currency": "EUR", "inclusion": "included" }
]
},
"priced_block": { "block": "139256509_91477112_2_34_0", "room": 139256509, "source": "hotelPage" }
},
"availability": { "status": "available", "requestedNights": 2, "minStayNights": null, "alternatives": [] },
"deep_link_url": "booking://hotel/1392565?affiliate_id=337862&checkin=2026-12-10&checkout=2026-12-12&mcid=10"
}

Add includeRooms: true to price from the full rate table instead of the recommended rate (priced_block.source becomes roomList), or childrenAges to monitor a family price.

Taxes and point of sale. Booking decides per point of sale whether taxes are part of the displayed price. Through Apify Proxy (US datacenter) the same Mercure rate comes back as total: 191.74 with the same three charges under inclusion: "excluded" (extra_charges.excluded: 84.23); from an EU IP they are included and total is 275.97. total + extra_charges.excluded is the all-in amount in both cases, so compare that when monitoring across runs.

3. Cheapest well-rated hotels in a city

Find the five cheapest hotels in Amsterdam scoring at least 8 and costing at most €300 for the stay. Rating, price and sort are applied client-side on top of Booking's own result order, so the actor keeps paging until it has enough.

{
"search": "Amsterdam",
"checkin": "2026-12-10",
"checkout": "2026-12-12",
"currency": "EUR",
"minReviewScore": 8,
"maxPrice": 300,
"sortBy": "price",
"maxItems": 5
}
{
"id": 2374707,
"name": "Hotel Levell",
"accommodationType": "Hotels",
"starRating": 3,
"reviewScore": 8.4,
"reviewScoreWord": "Very Good",
"reviewsCount": 15170,
"address": "…", "city": "Amsterdam", "countryCode": "nl",
"latitude": 52.36, "longitude": 4.88,
"currency": "EUR",
"checkin": "2026-12-10", "checkout": "2026-12-12",
"price": { "book": 113.86, "total": 113.86,
"extra_charges": { "excluded": 38.14, "included": 0,
"details": [ { "charge": "CITYTAX", "mode": "percentage", "total_amount": 14.23, "chargeTypeId": 22 },
{ "charge": "VAT", "mode": "percentage", "total_amount": 23.91, "chargeTypeId": 21 } ] } },
"freeCancellation": true,
"availability": { "status": "available", "soldOutMessage": null },
"url": "https://www.booking.com/hotel/nl/lowell.html?aid=337862&checkin=2026-12-10&checkout=2026-12-12&no_rooms=1&group_adults=2&selected_currency=EUR",
"deep_link_url": "booking://hotel/2374707?…"
}

Name, address, star rating, review score, coordinates and the persuasion badges (free cancellation, Preferred) come back on every search result at no extra cost — this plain record is the baseline; rooms, products, reviews and details are added only when you switch them on.

4. Apartments and holiday homes only

Restrict a destination search to self-catering places. accommodationTypes takes Booking's numeric type ids (201 = Apartments, 220 = Vacation Homes — see the full table); via the API you can also pass the names, e.g. "accommodation_types": ["Apartments", "holiday homes"]. With includeRooms each unit reports its bedroom and bathroom count.

{
"search": "Amsterdam",
"checkin": "2026-12-10",
"checkout": "2026-12-12",
"currency": "EUR",
"accommodationTypes": ["201", "220"],
"includeRooms": true,
"maxItems": 3
}
{
"id": 2197656,
"name": "houseboat Rose",
"accommodationType": "Apartments",
"accommodationTypeId": 201,
"address": "Amstel 171G", "city": "Amsterdam",
"price": { "total": 475.5, "…": "…" },
"rooms": [
{ "name": "Two-Bedroom Apartment", "maxOccupancy": 3, "fitsParty": true,
"bedroomCount": 2, "bathroomCount": 1, "privateBathroom": true,
"bedCount": 2, "hasSofaBed": false,
"beds": [ { "type": "double", "count": 2, "label": "2 full beds", "size": "131–150 cm wide" } ],
"surfaceM2": 60, "productCount": 1, "minPrice": 634.8, "currency": "EUR" }
],
"url": "https://www.booking.com/hotel/nl/houseboat-rose.html?…"
}

bedroomCount is null for ordinary hotel rooms (Booking publishes a count only for apartment-like units), so you never have to guess it from the room name.

5. Guest reviews for competitive analysis

Pull the 50 most recent guest reviews of a property, in British English, to feed a sentiment pipeline. Reviews are paginated 10 at a time, so maxReviews can go well beyond the first page.

{
"hotel_ids": ["1392565"],
"checkin": "2026-12-10",
"checkout": "2026-12-12",
"includeReviews": true,
"maxReviews": 50,
"language": "en-gb"
}
{
"id": 1392565,
"name": "Mercure Amsterdam Sloterdijk Station",
"price": { "…": "…" },
"reviews": [
{ "id": "a32af29c3af79834",
"title": "Excellent hotel close to Amsterdam, Haarlem as well as Schiphol.",
"pros": "Very friendly and helpful staf. Great clean rooms with plenty of space and a great bathroom.",
"cons": "N/a",
"score": 10,
"date": "2026-09-12",
"author": { "name": "Johan", "countryCode": "se", "countryName": "Sweden" } },
"… 49 more"
]
}

6. Radius search around coordinates

Everything within 1 km of Dam Square, nearest first. Coordinates run Booking's own lat/long search; the radius (km) cut-off is applied on the returned coordinates, and paging stops as soon as a whole page falls outside it.

{
"latitude": 52.3731,
"longitude": 4.8926,
"radius": 1,
"checkin": "2026-12-10",
"checkout": "2026-12-12",
"currency": "EUR",
"maxItems": 5
}
{
"id": 10502,
"name": "Swissôtel Amsterdam",
"latitude": 52.37350156398789,
"longitude": 4.893312156200409,
"address": "Damrak 96", "district": null, "city": "Amsterdam", "countryCode": "nl",
"reviewScore": 8.2, "starRating": 4,
"price": { "total": 353.31, "…": "…" },
"url": "https://www.booking.com/hotel/nl/swissotel-amsterdam.html?…"
}

district is Booking's neighbourhood label and is null where Booking has none (as for the Amsterdam centre); latitude / longitude are always present.

7. Full property details for enrichment

One property, everything static: address, stars, review score, coordinates, the complete facility list, photos, languages spoken, family amenities. On the hotel_ids path the actor also looks the property up by its coordinates to fill in the fields the hotel page itself lacks (web URL, star rating, review score, accommodation type, 100+ facilities, photos).

{
"hotel_ids": ["1392565"],
"checkin": "2026-12-10",
"checkout": "2026-12-12",
"includeDetails": true
}
{
"id": 1392565,
"name": "Mercure Amsterdam Sloterdijk Station",
"url": "https://www.booking.com/hotel/nl/mercure-amsterdam-sloterdijk-station-opening-july-2015.html?…",
"starRating": 4, "reviewScore": 8.6, "accommodationType": "Hotels",
"price": { "…": "…" },
"details": {
"name": "Mercure Amsterdam Sloterdijk Station",
"accommodationType": "Hotels", "accommodationTypeId": 204,
"starRating": 4, "reviewScore": 8.6, "reviewScoreWord": "Excellent", "reviewsCount": 3899,
"address": "Naritaweg 1, Westpoort, 1043 BP Amsterdam, Netherlands",
"city": "Amsterdam", "countryCode": "nl", "ufi": -2140479,
"latitude": 52.387911, "longitude": 4.834296, "timezone": "Europe/Amsterdam",
"facilities": ["Parking", "Room service", "24-hour front desk", "Fitness center", "Non-smoking rooms", "Laundry", "… 106 more"],
"familyFacilities": ["…"],
"languagesSpoken": ["pl", "nl", "fr", "es", "en-gb", "de", "ar"],
"isFamilyFriendly": false, "wifiReviewScore": 8.4,
"mainPhotoUrl": "https://cf.bstatic.com/xdata/images/hotel/max1024x768/873579185.webp?…",
"photos": ["…"],
"isBookingHome": null, "bedroomCount": null, "houseRules": null
}
}

For apartment-like properties details adds isBookingHome, isSingleUnitProperty, bookingHomeGroup, the unit's bedroomCount / bathroomCount and Booking's houseRules.

8. Sold out or minimum stay?

A property that cannot host the requested stay never shows up in a destination search — that is Booking's behaviour. On the hotel_ids path the actor instead always emits one record per id and explains itself in availability. Here Hotel Hortus enforces a 2-night minimum over New Year's Eve, and id 100 does not exist.

{
"hotel_ids": ["235613", "1392565", "100"],
"checkin": "2026-12-31",
"checkout": "2027-01-01",
"currency": "EUR"
}
[
{ "id": 235613, "name": "Hotel Hortus",
"checkin": "2026-12-31", "checkout": "2027-01-01", "datesAdjusted": false,
"price": { "book": null, "total": null, "extra_charges": { "excluded": 0, "included": 0, "details": [] } },
"availability": {
"status": "min_stay_not_met", "requestedNights": 1, "minStayNights": 2,
"soldOutMessage": "Sold out on Booking.com",
"alternatives": [
{ "checkin": "2026-12-31", "checkout": "2027-01-02", "nights": 2, "price": 440.22, "currency": "EUR" },
{ "checkin": "2027-01-02", "checkout": "2027-01-03", "nights": 1, "price": 209.63, "currency": "EUR" },
"…" ] } },
{ "id": 1392565, "name": "Mercure Amsterdam Sloterdijk Station",
"price": { "total": 314.57, "…": "…" },
"availability": { "status": "available", "requestedNights": 1, "minStayNights": null, "alternatives": [] } },
{ "id": 100, "name": null,
"price": { "book": null, "total": null },
"availability": { "status": "not_found", "message": "hotelPage: Hotel with id 100 was not found." } }
]

availability.status is one of available, min_stay_not_met (with minStayNights), sold_out, closed, no_fit_for_party (rates exist but none fits the party — only detectable with includeRooms) or not_found. alternatives are the nearby bookable stays Booking suggests. If Booking moved the dates, datesAdjusted is true and checkin / checkout carry the echoed dates. Not-found records are free (no search-result charge). On the search path, availability is { "status": "available" | "sold_out" } and the run log reports how many results were dropped client-side by your filters or radius.

Input reference

FieldDescription
searchMode 1. Destination name (city, region, landmark) — e.g. "Amsterdam". Also resolves specific hotel names, but matching a full/exact property name isn't guaranteed — for a specific property, prefer hotel_ids.
hotel_idsMode 2. Scrape specific properties by numeric Booking id instead of searching. Always yields one record per id (see scenario 8).
cityId / regionIdModes 3 and 4. Target a specific Booking destination by numeric id. Renamed from city/region; the old names still work.
latitude / longitude / radiusMode 5. Search around a GPS point, nearest first; radius in km is optional and applied client-side.
swLat / swLon / neLat / neLonMode 6. Sweep a custom rectangle. All four corners required together.

Two dropdowns, answering different questions.

mode — what this run does. Either a search, or one enrichment surface for properties you already have ids for:

modeDoesIts own ids field
search (default)find properties and price themhotel_ids (sub-mode below)
roomsroom pricing and availabilityhotelIdsRooms
reviewsguest reviewshotelIdsReviews (+ ufi)
reviewScoresreview score summary and subscoreshotelIdsScores
surroundingsnearby placeshotelIdsSurroundings
detailsstatic property detailshotelIdsDetails

Each mode has its own input group, and the mode dropdown sits above all of them. Nothing is shared between two modes: every mode has its own Hotel IDs field, and the options a mode needs are repeated in its group rather than borrowed from another — rooms has its own "only rates that fit" and "itemise taxes" switches, reviews its own "max reviews". A value left in one group cannot change what another mode does, and filling in the wrong group is refused by name:

Mode "rooms" works on properties you already have ids for.
Put them in "hotelIdsRooms" (the Hotel IDs field in that mode's own group).

Inputs that several modes legitimately read sit above the groups, next to the mode dropdown, rather than inside any one group where they would look mode-specific: the stay dates, guests, currency and language. A Search prices that stay, rooms re-prices it, and reviews uses it when no ufi is given.

The universal-but-rarely-touched settings — output shape, limits, file exports and the proxy — stay in their own groups at the end, so they do not sit between the mode dropdown and the mode groups.

hotel_ids still works as a fallback for every mode, so existing tasks and API callers keep running unchanged.

searchBy — how a search finds properties. Six sub-modes of search, ignored by the enrichment modes:

searchByFill in
auto (default)anything — the sub-mode is inferred
destinationsearch
hotelIdshotel_ids
cityId / regionIdcityId / regionId
pointlatitude, longitude, optional radius
areaswLat, swLon, neLat, neLon

auto keeps the original behaviour: the sub-mode is inferred from whichever field you filled in, and if you filled in more than one the run says which it used and which it ignored — in the log and the status message — instead of choosing silently:

You set 2 location modes. Used Hotel IDs; ignored destination name.

Precedence, highest first: hotel_ids → cityId → regionId → point → bounding box → search.

Choosing a sub-mode explicitly drops the other sub-modes' fields, so a leftover value cannot change where the run searches, and a sub-mode picked with nothing filled in is rejected rather than falling through to another one's input. searchBy was called searchMode; the old name still works.

Giving no location at all is rejected, with a message naming the fields you could set. It used to default to Amsterdam and bill you for 100 properties.

country was retired. Booking's mobile backend has no country-level search, so the field could only ever produce an error. Cover a country by its regions, or with a bounding box.

Flexible dates: a window instead of one stay

"The cheapest single night in the next 30 days" is a different question from "this exact night", and checkin/checkout can only express the second. Set windowDays and stayNights to ask the first:

// every single night in the next 30 days, for one hotel
{ "searchBy": "hotelIds", "hotel_ids": ["1392565"],
"windowDays": 30, "stayNights": 1,
"outputFields": ["price", "availability"] }
// two-night stays starting any day next week, across a destination
{ "search": "Ghent", "windowDays": 7, "stayNights": 2, "maxItems": 200 }

One row per property per stay, each carrying its own checkin/checkout, so the output is a rate calendar you can sort by price:

2026-09-26 -> 2026-09-27 total=150.58 available
2026-09-27 -> 2026-09-28 total=98.10 available
2026-09-28 -> 2026-09-29 total=134.10 available

The window starts today, or at checkin if you set one — a checkin already in the past is pulled forward to today rather than sweeping dates Booking cannot quote.

Each day in the window is a separate search, billed separately. A 30-day window costs roughly 30× a single-date run. Two things keep that in hand:

  • maxItems is a budget across the whole sweep, not per day. The run says when it stops early:
    maxItems (3) reached after 1 of 5 stays; the rest of the window was not searched.
  • windowDays is capped at 60.

Only the search and rooms modes accept a window — the others return the same answer whatever the dates, so sweeping one would bill the same record repeatedly, and that is refused with an explanation rather than run.

Enrichment modes: re-check what you already have

Each enrichment mode calls the one Booking endpoint that surface needs and bills only that surface — never a search result. Measured on one property:

modeRequestsCharged
rooms1room-details=5
reviews2 (1 if you pass ufi)review=3, review-scores=1
reviewScores1review-scores=1
surroundings1surroundings=1
details1hotel-details=1

search-result=0 in all five: an enrichment row carries one surface and no property summary, so there is no search result to bill.

// re-price a comp set every morning, no search
{ "mode": "rooms", "hotel_ids": ["1392565", "235613"],
"checkin": "2026-11-20", "checkout": "2026-11-22" }
// reviews without the price payload; ufi from an earlier search run
{ "mode": "reviews", "hotel_ids": ["1392565"], "ufi": "-1958757", "maxReviews": 50 }
// nearby places only
{ "mode": "surroundings", "hotel_ids": ["1392565"] }

Which surfaces need a search at all, once you have hotel ids:

SurfaceNeeds a search?Turn on with
Price and availabilityno(always returned)
Rooms and ratesnoincludeRooms
Guest reviewsnoincludeReviews
Review scores and subscoresnoincludeReviewScores
Nearby placesnoincludeSurroundings
Static detailsyes, one lookup per propertyincludeDetails

Static details is the one exception: Booking's property endpoint carries no slug, star rating, review count, property type or photo list, so on the search path those are read from the search result for the same property, found by its coordinates. mode: "details" skips that lookup and returns the static content endpoint alone.

Why reviews wants a ufi. Booking's review endpoint returns nothing without the destination's ufi, so the mode needs one. Paste it from an earlier search run's output and each property costs a single request; leave it empty and one extra lookup per property derives it. A ufi identifies a destination, so one value covers every property in it.

Exports by mode: what Excel and KML actually contain

Both exports are built from the rows a run delivers, so what they hold depends on which mode produced those rows. The map export is empty in every enrichment mode — enrichment rows carry no coordinates, so there is nothing to place. Measured on one property, with both exports on:

modeExcel sheets that fillMap (KML)
searchProperties, and Rates / Rooms / Reviews for whatever was includedworks — one pin per property
roomsRates (33 rows), Rooms (5 rows); Properties holds the id only0 placemarks
reviewsReviews (2 rows); Properties holds the id only0 placemarks
reviewScoresnothing beyond the id0 placemarks
surroundingsnothing beyond the id0 placemarks
detailsnothing beyond the id0 placemarks

Two things follow.

The Properties sheet is near-empty in enrichment modes. It keeps its full column set — name, stars, review score, address, coordinates — but an enrichment row carries none of those, because fetching them is what the search path is for. The row is there so the sheet's ids line up with the other sheets; do not read it as "the property has no name".

Nearby places, review scores and static details have no sheet of their own. They are nested structures rather than tables, so they reach you through the dataset (JSON/CSV via the API or the Console), not through the workbook.

The run says so as it goes. A map export with nothing to place logs it rather than leaving you to open the file:

Map export ready (0 placemarks): ...
1 property was left off the map for having no coordinates.

If you want a map of properties you have ids for, run mode: "search" with searchBy: "hotelIds" — that path returns coordinates, so the pins land.

Modular output: take only the blocks you need

By default every record carries everything — pricing, property facts, rooms, reviews, scores and nearby places, around 28 top-level keys. That is right for exploring and wrong for a job that runs every morning and wants a price and an availability status.

outputFields narrows the record to the blocks you pick:

BlockKeeps
priceprice, freeCancellation, geniusRate, dealBadges
availabilityavailability (status, min stay, alternatives)
propertystars, review score, address, coordinates, accommodation type
roomsrooms, products — the full rate table
detailsfacilities, photos, house rules, languages
reviewsreviews
reviewScoresreviewSubscores, reviewSummary
surroundingsnearby places

id, name, url, the stay dates, the currency and datesAdjusted are always kept, so rows stay joinable and re-checkable. datesAdjusted in particular is never dropped: it says Booking served a different stay than the one you asked for, which is what makes a price comparable or not.

Recipes:

// price monitoring — 12 keys instead of 28
{ "searchMode": "hotelIds", "hotel_ids": ["1392565", "235613"],
"outputFields": ["price", "availability"] }
// availability checking only
{ "searchMode": "hotelIds", "hotel_ids": ["1392565"],
"outputFields": ["availability"] }
// review mining, no pricing payload
{ "searchMode": "destination", "search": "Ghent",
"includeReviews": true, "includeReviewScores": true,
"outputFields": ["reviews", "reviewScores"] }

This changes what you receive, not what you are charged. Billing follows what was fetched and delivered: the search still ran, so the search result is still billed. To spend less, turn off the include* toggles so the extra requests are never made in the first place — narrowing the output afterwards does not refund work already done.

One exception worth knowing: if exportKml is on, coordinates are kept even when you did not select the property block, because a KML without them is an empty map.

Upgrading: what changed for existing runs

Search-mode runs are unchanged. A saved task sending search, dates, guests and the include* toggles produces the same records, the same fields and the same charges as before — verified by running the old and new builds side by side on the same input and diffing the output structure property by property.

Three things did change, and only one of them can affect a run you already have scheduled:

  1. Two flags that did nothing now work — and are billed. includeSurroundings without includeDetails, and includeReviewScores without includeReviews, used to be silent no-ops: flag on, no request, no data, no charge. They now fetch what you asked for, which means a run with one of those combinations starts returning data and paying the matching per-item charge. If you had one set by accident, turn it off.
  2. No location is rejected instead of defaulting to Amsterdam. A run with no search, hotel_ids, ids, point or area used to return 100 Amsterdam properties and charge for them. It now finishes with an explanation and charges nothing.
  3. country was retired. It never worked — Booking's mobile backend has no country-level search — so it only ever produced an error.

Renames are aliased, not breaking: city and region still work alongside cityId and regionId. searchMode defaults to auto, which is the old inference. outputFields left empty gives the full record, which is the old shape.

How many properties can you actually get?

Booking's search stops returning new properties at roughly 1,000 per destination, and it does not tell you it has stopped: past that point it keeps answering with full pages of properties it has already sent. Every scraper built on this API inherits that ceiling.

deepSearch works around it by asking several narrower questions instead of one broad one. The actor reads how many properties the destination actually holds, splits it by star rating, and splits any slice still too large into price bands

  • bisecting until every slice fits under the ceiling - then pages each one and merges the results. Every slice keeps the destination, so results never wander into a neighbouring city, and a property is returned, and billed, only once however many slices happen to find it.

Because Booking reports the destination's true total, a run can state its own coverage rather than guess at it. The log ends with, for example, 5,822 of 5,844 properties (99.6%).

Measured 2026-09-23, check-in 2026-11-10, two nights:

DestinationAvailableReturnedCoverageSlices
Singapore40140099.8%none needed
New York52251999.4%none needed
Jakarta2,6592,63699.1%10
Tokyo5,8445,82299.6%28

Planning is cheap - Tokyo costs 54 one-row count queries - and a destination under the ceiling is detected in a single query, so a small town triggers none of this and costs exactly what it did before.

Two honest caveats. Slices overlap, because a property matches a price band if any of its room rates falls inside it, so the run fetches more rows than it delivers; de-duplication means you are never billed twice, but the requests are real. And the mobile API this actor uses holds slightly less inventory than booking.com's website - 5,844 against 6,084 for Tokyo - so 100% here means 100% of what the API offers.

What a destination covers, and what it does not

A destination is Booking's own idea of a place, and it can be narrower than the name suggests. "New York" is a city destination holding ~520 properties; the surrounding metro - Jersey City, Hoboken, Brooklyn, North Bergen - sits under separate destinations and is not reachable through it.

Three destination shapes work, and free text resolves to whichever fits:

inputexamplenotes
search"Bali", "Tokyo"resolves to a city or a region automatically
cityId-2140479a numeric destination id, if you already know it
regionId835a numeric region id - broader than a city

Autocomplete matches what Booking has a destination for, which is not always what you would guess: "Queens" resolves to Queenstown, New Zealand, while "Manhattan", "Newark" and "Long Island City" resolve to nothing at all. The run logs what it resolved to — read that line before trusting a sweep. For ground Booking has no destination for, use the area corners.

For an area with no destination of its own, use latitude/longitude with a radius: results come back nearest-first and are trimmed to the radius.

Two gaps worth knowing before you plan around them:

  • Countries are not searchable. Booking's mobile backend has no country destination, so "Lithuania" resolves to nothing. Cover a country by its regions or cities instead.
  • Districts usually are not either. "Shinjuku", "Manhattan" and "Newark" all resolve to nothing, while "Brooklyn" and "Jersey City" are their own destinations. If a district fails, search its city.

When a run returns nothing

A run that could not have worked finishes successfully with an empty dataset and an explanation in its status message, rather than failing. You are not charged for it, and it does not count against the Actor's failure rate - a mistyped city is your input to fix, not a fault in the Actor.

You will see this for a date that will not parse or is in the past, a check-out before its check-in, a destination that cannot be resolved, or hotel_ids that are not numeric Booking ids. The status message names the problem in each case, for example Nothing to scrape: could not resolve destination: 'Delfr'.

An empty dataset from a valid search means something different: the search genuinely matched nothing for those dates. Widen the dates or drop filters.

What each charge event pays for

Every optional surface that costs a separate API request is its own charge event, so you pay for what you actually asked for and nothing else:

eventchargedcovers
Search resultper accommodation returnedthe property result with live pricing
Hotel detailsper propertytwo requests - property page + static content - deliberately billed as one
Room detailsper room optionthe per-rate products list comes from the same request and is not charged again
Reviewper review returnedthe paginated review list
Nearby placesper propertyincludeSurroundings only
Review scoresper propertyincludeReviewScores only, one request regardless of review count

A surface that fails or comes back empty is free - charging happens on delivery, not on the request.

Changed in 1.0.50. details.surroundings, reviewSubscores and reviewSummary used to be fetched automatically with includeDetails / includeReviews and were not charged. They are now opt-in and billed separately. Runs that do not set the new toggles get cheaper and no longer receive those three fields; set includeSurroundings / includeReviewScores to keep them.

Accommodation types

accommodationTypes takes Booking's numeric type ids as strings ("201"); the snake_case spelling accommodation_types also accepts integers and the names below (and common synonyms such as apartment, B&B, aparthotel, holiday home, guest house, campsite, glamping), case-insensitively. Names are Booking's own en-us labels, verified live on 2026-09-17 across 15 destinations; the accommodationType field of every result uses the same labels.

IdNameAlso accepted as
201Apartmentsapartment, flat
203Hostelshostel
204Hotelshotel
205Motelsmotel
206Resortsresort
208Bed and BreakfastsB&B, bed & breakfast
209Ryokansryokan
210Farm Staysfarm stay, farmstay
212Resort Villagesholiday park
213Villasvilla
214Campgroundscampsite, camping
215Boatsboat
216Guesthousesguest house
218Innsinn
219Condo Hotelsaparthotel, apart-hotel
220Vacation Homesholiday home, vacation rental
221Lodgeslodge
222Homestayshomestay
223Country Housescountry house
224Luxury tentsglamping
225Capsule Hotelscapsule hotel
226Love Hotelslove hotel
227Riadsriad
228Chaletschalet

Output reference

Each record is a search-response object: id, currency, checkin, checkout, deep_link_url, url, price, availability, plus the free summary fields (name, accommodationType, starRating, reviewScore, reviewsCount, address, city, district, countryCode, latitude, longitude, freeCancellation, isPreferred…) and, when switched on, rooms, products, reviews, details.

price — base (net), book / total (what you pay for the stay), extra_charges.included / excluded (summed) and extra_charges.details[], one entry per tax or fee: charge (CITYTAX, VAT, SERVICECHARGE, TOURISMFEE, CLEANINGFEE, …), localised name, mode (percentage, per_night, per_person_per_night, per_stay), percentage or unit_amount, total_amount, currency, inclusion. On the search path total is Booking's display price before the listed charges (total + extra_charges.excluded = the gross rate; entries carry Booking's numeric chargeTypeId, no localised name); on the hotel_ids path total is the rate as Booking displays it for the run's point of sale (taxes included from an EU IP, excluded through a US proxy — see the note in use case 2) and priced_block says which rate it came from. total + extra_charges.excluded is always the all-in amount.

rooms[] — one entry per room type, always the full catalogue: id, name, maxOccupancy, fitsParty, productCount, minPrice, minPriceFitting, bedroomCount, bathroomCount, privateBathroom, beds[] ({type, typeId, count, name, label, size, configuration} with type one of twin, double, queen, king, sofa_bed, bunk_bed), bedCount, hasSofaBed, raw bedConfigurations, surfaceM2, photos, childrenPolicy (maxChildrenFree, maxChildrenFreeAge), extraBedAvailable, extraBedPrice, babyCotAvailable. Bed and bedroom data is taken from that room type only.

products[] — one entry per bookable rate: id, room (join to rooms[].id), roomName, fitsParty, fitStatus, occupancy (adults, children, childrenAges, requestedAdults, requestedChildrenAges), maxOccupancy, bedroomCount, bathroomCount, number_of_adults, children, number_available_at_this_price, deal, policies (cancellation, meal plan, payment), price with itemised extra_charges.included[] / excluded[].

Searching an area instead of a destination

Set swLat / swLon / neLat / neLon to sweep a rectangle. Use it when the ground you want has no destination of its own — Booking's "New York" city destination holds ~520 properties, while a box over the same map also reaches Jersey City, Hoboken, Brooklyn and North Bergen.

It costs much more per property than a destination search. The area is covered by point searches, and each one is answered with the properties nearest its centre — including everything just outside your rectangle, which you pay nothing for but which still consumes that search's result ceiling. A 21×28 km box over New York took 30 searches to return ~1,000 properties; the same number from a destination takes a handful. Keep boxes tight and set maxItems.

What it costs, measured. Same box and dates, properties actually inside the rectangle:

areasizesearchesproperties
Midtown Manhattan2×2 km1169
New York metro21×28 km14~996
Lithuania380×283 km274,624 (capped by maxItems)

A small box can be finished in a single search; a country takes tens. The number of searches follows how far each one reaches, not how big the box is.

The limitation worth knowing before you rely on this. In a dense city centre a search reaches only as far as the ~1,000-result ceiling allows — about 2 km in Manhattan — so a large dense box can never be proved covered, and splitting it further barely helps: going from 14 searches to 113 on the New York box moved the count from 996 to 1,004, under 1%. For a dense city, a destination search (search: "New York") with deepSearch is both cheaper and more complete; an area sweep is the right tool when the ground you want has no destination, or spans several.

What the run tells you about coverage. A destination search can say "5,822 of 5,844". An area search cannot — Booking reports no total for a coordinate query. Instead each cell reports how far its search actually reached against how far it needed to:

cell 7/128 (40.681,-73.881, depth 2): 6 new, reached 11.3 km of 4.4 needed

Reaching further than the cell's own corner means every property inside that cell was within range, so the cell is completely covered. When that holds for every cell, the run says so. When it does not, the run says that too, and names the reason — either the cell budget ran out, or the area stopped yielding because in a dense centre a search reaches only as far as the result ceiling allows, and no amount of further splitting gets past it.

Downloadable files: Excel and map exports

Two optional files, both off by default and both free — they reshape the data you already paid for and add no charge events. Switch either on and find it in the run's Storage → Key-value store tab when the run finishes.

Excel workbook (exportExcel) — an .xlsx with one sheet per section, so nested data arrives as rows you can pivot instead of JSON crammed into a cell:

SheetOne row perCreated when
Propertiesproperty — name, stars, review score, address, coordinates, price, availability, linksalways
Ratesbookable rate — room, meal plan, cancellation terms, price, taxesincludeRooms
Roomsroom type — occupancy, beds, bathrooms, size, lowest priceincludeRooms
Reviewsguest review — author, score, date, title, pros, consincludeReviews

Child sheets carry property_id, so a VLOOKUP (or a pivot) joins them back to Properties. The header row is frozen and filtered.

Map file (exportKml) — a .kml with one placemark per property, ready to open in Google Earth, Google My Maps or QGIS. Each pin's balloon carries the total price, review score, address and a link straight to the property — and a photo too when the run has includeDetails on, since the photo URL only comes back with the property details. Pins are grouped into folders by city, and kmlColorBy shades them by price (this run's own quartiles — cheapest green, dearest red) or by review score (fixed bands: 9+ green, 8–9 amber, 7–8 orange, below 7 red). Properties without coordinates are skipped and the count is logged.

{
"search": "Vilnius",
"checkin": "2026-11-10",
"checkout": "2026-11-12",
"includeRooms": true,
"exportExcel": true,
"exportKml": true,
"kmlColorBy": "review_score"
}

Pricing – pay only for the data you get

This scraper uses pay-per-event billing. You're charged only for results actually delivered — never for failed requests or unknown ids.

You payPerEquivalent
$0.001hotel search result$1.00 / 1,000
$0.001property details record$1.00 / 1,000
$0.0001room option$0.10 / 1,000
$0.0001guest review$0.10 / 1,000

There is no per-run start fee — every charge above is for an item actually delivered. A plain search result costs a tenth of a cent; rooms, reviews and details add their tiny per-item charge only when you ask for them, so a lightweight price check stays extremely cheap and a full property profile is still a fraction of a cent. Unknown hotel ids and failed requests are not charged.

Apify platform usage (compute units, proxy, data transfer) is billed on top of the per-event prices, as shown on your run's usage tab.

Coming 8 October 2026

Two optional surfaces that are currently returned free start being charged, at the rates below. The four prices above do not change, and nothing you are already running gets more expensive unless you opt into these:

You payPerEquivalentOpt in with
$0.0005nearby-places record$0.50 / 1,000includeSurroundings
$0.0005review-scores record$0.50 / 1,000includeReviewScores

Frequently asked questions

Is there a Booking.com API, and do I need partner approval to use it? Booking.com's official Demand API is partner-gated — you need a contract and an affiliate ID. This actor needs neither: it reads Booking.com's public mobile app API (mobile-apps.booking.com) and returns structured JSON, so you can get hotel prices without an API key.

How do I scrape Booking.com hotel prices? Give the actor a destination and dates and it returns every property that can host the stay, each with its live total price, currency and booking link. Add includeRooms for the per-room rate table and includeExtraCharges for the itemised taxes and fees.

How do I get room rates and availability for a specific hotel? Pass the property's numeric id in hotel_ids with your dates and set includeRooms. You get every bookable rate — occupancy, bed layout, board, cancellation policy, rooms left — and, when there is no price, an availability status saying why.

Can I search hotels near a set of coordinates? Yes. Set latitude, longitude and radius to search by GPS radius, or pass neLat/neLon/swLat/swLon for a bounding box. Useful for "hotels within 2 km of this venue" rather than "hotels in this city".

Which Booking.com scraper should I use for rate shopping or revenue management? One that returns rates per room rather than one price per hotel, because a blended nightly total cannot be compared across boards, cancellation policies or occupancy. This actor returns the full per-room rate table with taxes itemised, which is the level rate-shopping actually needs. See the comparison above.

Do I need a Booking.com account or API key? No. Just configure the input and run it.

Does it price children correctly? Yes — pass the ages in childrenAges (or guests.children) and every request carries them; see Children / family stays. Without ages Booking prices the party as adults only.

Can I get prices in my own currency and language? Yes — set currency and language to whatever you need.

How many reviews can I collect? Reviews are paginated, so you can pull well beyond the first page — set maxReviews to the number you want per property.

Can I scrape a specific hotel? Yes — pass its Booking id in hotel_ids to fetch that property directly. This must be the numeric internal id, not the URL slug or property name. To find it: open the hotel's Booking.com page, use your browser's View source (or right-click → View Page Source), and search for hotelId — the number next to it is the id to use. An unknown id yields a record with availability.status: "not_found" instead of silently vanishing.

Can I just put a hotel's name in search instead of finding its id? You can, and it often works — the actor also tries to resolve a full hotel name (e.g. one copied from a listing site) to the right property, with checks in place to avoid confidently returning the wrong one. But name matching isn't guaranteed to hit, and on ambiguous or unusual names the run may fail with "Could not resolve destination" instead. Passing the numeric id in hotel_ids is the reliable way to target one specific hotel.

Why is a hotel missing from my search results? Booking's search only returns properties that can host the requested stay. To learn why a specific property is missing (sold out, minimum stay, closed), query it through hotel_ids and read availability.

Does it return phone numbers or email addresses? No. Booking.com hides direct property contact details until after a booking, so they aren't available from public data.

What formats can I export? Results are stored in a dataset and can be downloaded as JSON, CSV, Excel or HTML, or accessed via the API and integrations.

Is this an official Booking.com product? Does it use the Demand API? No to both. It's an independent scraper that reads Booking.com's public mobile app API (mobile-apps.booking.com); it does not call the partner-gated Demand API and needs no affiliate ID, API key or partner approval.

Travel & booking toolkit

Part of the Travel & booking toolkit — search and pricing data across accommodation, transport, and travel-planning sites worldwide:

Did you find this useful?

Rate this actor on Apify! Your feedback helps other users find it and helps us keep improving it.

Use this scraper to collect publicly available information only, in line with Booking.com's Terms of Service and applicable laws (including data-protection rules). Avoid collecting personal data without a lawful basis, and scrape at a reasonable rate. You are responsible for how you use the data you extract.