# Booking.com Hotel Scraper – Prices, Room Rates & Reviews (`rl1987/booking-api-scraper`) Actor

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.

- **URL**: https://apify.com/rl1987/booking-api-scraper.md
- **Developed by:** [R.L.](https://apify.com/rl1987) (community)
- **Categories:** Travel
- **Features:** Limited permissions
- **Stats:** 493 total users, 208 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.54 / 1,000 search results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

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

**Scrape Booking.com at scale** — hotels, live room prices, availability, full
rate breakdowns, facilities, photos and guest reviews as clean, structured
**JSON**. No login, no captchas, no browser automation. Give it a destination and
dates and export to **CSV, Excel, JSON or the API**.

Whether you track competitor prices, build a travel app, feed a rate-shopping
dashboard or do 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`) instead of rendering pages, so it needs no browser
> automation or login and is far faster and more stable than page-rendering
> scrapers.
> **No partner contract needed** — it does **not** call the partner-gated Demand
> API and needs no affiliate ID, API key or approval.

### 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 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 your
  currency.
- **Family stays priced by child age** – pass the ages and get the rates Booking
  would actually sell *your* party, family rooms surfacing and non-fitting rooms
  flagged.
- **Rooms and availability** – every room type and bookable rate: occupancy,
  structured bed layout, 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,
  saying *why* there is no price: sold out, minimum stay not met (with the nights
  required), closed, or unknown id.
- **Guest reviews** – paginated, with score, title, positive/negative text, date
  and reviewer country.
- **Property details** – name, star rating, review score and count, coordinates,
  address, 100+ named facilities, family amenities, languages, house rules,
  photos.

### Example use cases

- **Hotel price monitoring & rate shopping** – track competitor and market
  prices over time for revenue management.
- **Family & group travel products** – price parties with children correctly, and
  show only rooms that fit.
- **Travel & metasearch products** – power a booking site, app or 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.
- **Lead lists & data enrichment** – build accommodation datasets with geo and
  facilities context.

### Why this scraper, not a flat-listing one

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

- **It tells you why a hotel has no price.** Query a fixed list of ids and you
  always get a record back — sold out, minimum stay not met (with the nights
  required), closed, or unknown id — instead of the property vanishing.
- **It searches by GPS radius or bounding box**, not just destination name.
- **It is one actor, not four.** Search, details, rooms and reviews come from the
  same run, so you do not chain a listing scraper into a reviews scraper into a
  photos scraper.
- **There is no per-run start fee.** That matters more than it looks on a
  schedule: a start fee is charged whether a run returns 2 rows or 2,000, so it
  falls hardest on exactly the small, frequent runs price monitoring is made of.
  Most alternatives charge one.

### How to use it

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

#### Family stays with children

**Child ages are required for correct family prices.** Booking prices a stay by
each child's age — free cots, kids-stay-free rules, extra beds — so 2 adults
with children aged 2 and 9 pays differently from the same party aged 12 and 15,
and differently again from 4 adults.

```json
{ "adults": 2, "rooms": 1, "childrenAges": [5, 9] }
```

The ages go out on every request, rooms that cannot host the party are marked
`fitsParty: false`, and each product echoes `occupancy.requestedChildrenAges` so
you can confirm they reached Booking. The alternative spellings, the per-room
allocation form and what happens when you omit the ages are in
**[the input reference](https://keyspace.dev/data-dictionaries/booking-com/input-reference.html)**.

#### Ready-made examples (one click, no setup)

Nine published tasks sit on this Actor's **Examples** tab: rate-shopping a fixed
compset room by room, city-wide price monitoring, sell-out tracking for
compression dates, full rate tables with taxes, reviews with per-category
subscores, cheapest well-rated properties, family pricing by child age,
self-catering supply with bedroom counts, and a landmark radius search. Open
one, press run, and it works with the input already filled in; running it copies
it into your account, so you can change anything afterwards without touching the
original. None pins a date — check-in defaults to 30 days ahead, so they return
live availability rather than going stale.

**Dates, absolute or relative.** Every date input — `checkin`, `checkout`,
`attractionsStartDate`, `attractionsEndDate` — takes either a chosen day
(`2026-12-10`) or a relative value (`30 days`, `2 weeks`, `1 month`). Prefer the
relative form in anything saved or scheduled: a fixed date is correct the day you
set it and then drifts into the past, and Booking only quotes future stays, so a
schedule built on one eventually rejects its own input. Both are resolved to a
real date before any request goes out, and every row carries that date, not the
words.

### Recipes

Eight worked runs — family stays with child ages, price monitoring over a fixed
list, cheapest well-rated hotels in a city, apartments only, reviews for
competitive analysis, radius search around coordinates, full enrichment, and
telling sold-out from minimum-stay — each with the exact input that produces it,
are on **[recipes](https://keyspace.dev/data-dictionaries/booking-com/recipes.html)**.

### Input reference

**Every input, with its type and default, is on the
[Input](https://apify.com/rl1987/booking-api-scraper/input-schema) tab** — it is
generated from the schema, so it describes the deployed build. What a field
description cannot carry is on **[the input reference](https://keyspace.dev/data-dictionaries/booking-com/input-reference.html)**:
how the ten modes differ, which toggles add a charge event, where a run's
limits come from, and what Deep search actually does.

The first decision is the **mode**, and it is also the one that sets the bill:

| mode | what it does | bills |
|---|---|---|
| `search` | find properties and their prices | `search-result` |
| `rooms` | room options for ids you have | `room-details` |
| `rates` | re-check named product ids | `rate-check` or `room-details` |
| `reviews` | guest reviews | `review` + `review-scores` |
| `reviewScores` | score summary only | `review-scores` |
| `surroundings` | nearby places | `surroundings` |
| `details` | static property content | `hotel-details` |
| `attractions` | tickets, tours, activities | `attraction-row` |
| `attractionDetails` | one attraction in full | `attraction-detail` |
| `attractionAvailability` | an attraction's dates | `attraction-availability` |

On `search` and `rooms`, each `include*` toggle adds its own event — that is
where a run's cost actually comes from. **Charging happens on delivery, not on
request**: a surface that fails or returns nothing is free. Units, the toggles
and the two events that are not priced yet are in
**[what each mode bills](https://keyspace.dev/data-dictionaries/booking-com/billing-by-mode.html)**; the figures are on the
Pricing tab, which is generated and plan-tiered.

### 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, fields and charges** as
before — verified by diffing old and new builds on the same input, property by
property.

**Breaking changes, all of them.** Each one removes or refuses something that was
wrong rather than merely unhelpful, and each is loud instead of silent:

| what | why |
| --- | --- |
| attractions: `latitude` / `longitude` **renamed** to `cityLatitude` / `cityLongitude` | they are the destination centroid, not the product: 21 products in three cities gave three pairs, one per city. A product's own are on an `attractionDetails` row |
| attractions: `cancellationLabel` removed | null on **143 of 143** products; `freeCancellation`, `stillRefundable` and `cancellationUntil` fill every time |
| `scrapedAt` / `scrapedAtUnix` **added** (not a breaking change) | ISO-8601 UTC to milliseconds, and epoch seconds, on every row: when that row's price was fetched, not when it was pushed. Booking's own date strings are **not** ISO — do not share a parser. `RUN_SUMMARY.window` bounds the run |
| `extra_charges.conditional[]` and `conditionalTotal` **removed** | empty everywhere: 0 of 4,934 room-list lines, 0 of 405 search lines. A line's own `inclusion` still reads `conditional`, and such a line now groups with `excluded`, never `unknown`. `depositTotal` stays — a refundable deposit there would overstate the bill |
| `bookerCountry` **removed** | read by nothing since the day it was added — never in `src/`, never on a request. The point of sale follows the **proxy exit country**, now reported per row as `provenance.exitCountry` |

Older waves — the 1.0.94–1.0.141 changes, the two no-op flags that now work and
are billed, the rejected-instead-of-defaulted location, and the retired
`country` — are in **[the upgrade guide](https://github.com/rl1987/apify-booking-api-scraper/blob/main/docs/upgrading.md)**, with the reasoning for each.

Renames are aliased, not breaking: `city` and `region` still work beside
`cityId` and `regionId`, `searchMode` defaults to `auto`, and an empty
`outputFields` gives the full record.

### Output reference

**Every column, with its type and what it means, is on the
[Output](https://apify.com/rl1987/booking-api-scraper/output-schema) tab** —
120 of them, generated from the dataset schema, so it describes what the
deployed build returns. The same tables by row kind, each with a real row and
the nested objects member by member, are on keyspace.dev:

| row kind | modes | columns | reference |
|---|---|--:|---|
| Accommodations | `search`, `rooms`, `details`, `surroundings` | 35 | [property-rows](https://keyspace.dev/data-dictionaries/booking-com/property-rows.html) |
| Reviews & scores | `reviews`, `reviewScores` | 12 | [reviews](https://keyspace.dev/data-dictionaries/booking-com/reviews.html) |
| Watched rates | `rates` | 27 | [rates](https://keyspace.dev/data-dictionaries/booking-com/rates.html) |
| Attractions | `attractions` | 30 | [attractions](https://keyspace.dev/data-dictionaries/booking-com/attractions.html) |
| Attraction details | `attractionDetails` | 56 | [attraction-details](https://keyspace.dev/data-dictionaries/booking-com/attraction-details.html) |
| Attraction availability | `attractionAvailability` | 19 | [attraction-availability](https://keyspace.dev/data-dictionaries/booking-com/attraction-availability.html) |

Numeric ids — accommodation type, room type, bed type, charge type, payment card
— are tabled in [the integer ID reference](https://keyspace.dev/data-dictionaries/booking-com/id-reference.html). Annotated
screenshots joining each number on screen to the field behind it are on
[the screenshots page](https://keyspace.dev/data-dictionaries/booking-com/screenshots.html).

**What is inside the amount the guest pays, what Booking adds on top, and what
sits outside it entirely, is drawn in [the price stack](https://keyspace.dev/data-dictionaries/booking-com/price-stack.html).**
Read that before reconciling a total against anything.

#### The five columns read wrong most often

Everything else is in the references above; these five cost money to get wrong.

- **Compare against `price.total_with_charges`, not `price.total`.** `total` is
  the display price: right on a room-list rate, low wherever Booking excludes
  tax from it. One column is correct on both paths.
- **`starRating` does not say whether the stars are official.** Booking awards
  some and lets others self-estimate; the surface returns `symbol: "STARS"` for
  both, so ranking by it mixes them.
- **`amountDueNow` is `null`, not `0`,** when a rate states no payment stage.
  Treating null as zero turns every such rate into a false "pays nothing now".
- **A meal price is in the property's own currency, not the row's.** Read
  `breakfastPrice` with `breakfastCurrency`: a Cape Town property quotes 120 ZAR
  on a run priced in EUR.
- **Do not rebuild `url` — read it.** Booking's slugs are not derivable from the
  name, and unavailable ones come back `null` rather than guessed.

### Known limitations

What this surface cannot give you — each entry with the measurement that says
so, and tagged by who could fix it — is on
**[known limitations](https://keyspace.dev/data-dictionaries/booking-com/known-limitations.html)**. The one worth knowing
before you start: **`roomList` returns the room types bookable for your dates,
not the property's catalogue**, and the shortfall is not detectable from the
payload. So "this property has N room types" is not a claim this data supports.

### Attractions: tickets, tours and activities

Booking sells more than places to stay. Three modes read its bookable-product
catalogue — museum admissions, guided tours, day trips, transfers. A different
vertical from the six property modes, sharing nothing with them but the location
primitive: no stay window, no guests, no room blocks.

| mode | gives you |
|---|---|
| `attractions` | the catalogue for a place, with price, rating, duration and cancellation terms |
| `attractionDetails` | one product in full — the long description, what is included, where it starts |
| `attractionAvailability` | which dates are bookable, and optionally the timeslots |

How to point them at a destination, what each input does and how to read the
availability `status` are in **[the input reference](https://keyspace.dev/data-dictionaries/booking-com/input-reference.html)**;
the columns are on **[attractions](https://keyspace.dev/data-dictionaries/booking-com/attractions.html)**,
**[attraction details](https://keyspace.dev/data-dictionaries/booking-com/attraction-details.html)** and
**[attraction availability](https://keyspace.dev/data-dictionaries/booking-com/attraction-availability.html)**.

### Frequently asked questions

#### Can I get Booking Genius / loyalty-tier prices?

**No — and that is a proven absence, not a gap in this Actor.** Genius prices
need a member login, which this Actor deliberately never does. The evidence —
`genius_discount_percentage` present and exactly `0` on 1,385 of 1,385 blocks,
0 of 180 search rows badged, and no request parameter that sets a level — is on
**[known limitations](https://keyspace.dev/data-dictionaries/booking-com/known-limitations.html)**, which collects everything
this surface cannot give.

**Is there a Booking.com API, and do I need partner approval to use it?**
Booking.com's official Demand API is partner-gated — contract and affiliate ID.
This actor needs neither: it reads the 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 it a destination and dates and it returns every property that can host the
stay, with live total price, currency and booking link. Add `includeRooms` for the
per-room rate table and `includeExtraCharges` for itemised taxes and fees.

**How do I get room rates and availability for a specific hotel?**
Pass its 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, where 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
`neLat`/`neLon`/`swLat`/`swLon` for a bounding box — "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 returning 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.
See [why this scraper](#why-this-scraper-not-a-flat-listing-one).

**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](#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`.

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

**Can I scrape a specific hotel?**
Yes — pass its numeric Booking id (not the URL slug or name) in `hotel_ids`. To
find it, open the hotel's Booking.com page, **View source**, and search for
`hotelId`. An unknown id yields `availability.status: "not_found"` rather than
silently vanishing.

**Can I just put a hotel's name in `search` instead of finding its id?**
Often, yes — the actor tries to resolve a full hotel name to the right property,
with checks against confidently returning the wrong one. But matching is not
guaranteed, and ambiguous names may fail with "Could not resolve destination".
The numeric id in `hotel_ids` is the reliable way to target one hotel.

**Why is a hotel missing from my search results?**
Booking's search only returns properties that can host the stay. To learn *why*
one 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 property contact details until after a booking, so they are
not in public data.

**What formats can I export?**
Results land in a dataset, downloadable as JSON, CSV, Excel or HTML, or available
through the API and integrations.

**Is this an official Booking.com product? Does it use the Demand API?**
No to both — it is an independent scraper reading the public mobile app API, and
needs no partner approval.

This is one entry in a longer inventory: **[known limitations](https://keyspace.dev/data-dictionaries/booking-com/known-limitations.html)** collects everything this surface cannot give, each with the measurement behind it and tagged by whether it is Booking's limit, ours, or a line we chose not to cross.

### Did you find this useful?

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

# Actor input Schema

## `mode` (type: `string`):

Pick Search for the normal flow, or an enrichment mode to re-check properties you already have ids for without searching again. *Watch rates* is the one keyed on individual rates rather than whole properties, for price and availability monitoring.

## `checkin` (type: `string`):

Check-in date. Pick a day, or switch the picker to a relative value like "30 days" — relative is the one to use in a saved task or a schedule, since a fixed date eventually falls into the past and Booking only quotes future stays. Empty = 30 days from today.

**Used by more than one mode:** a Search prices this stay, *Room pricing & availability* re-prices it, and *Guest reviews* uses it only when no `ufi` is given.

## `checkout` (type: `string`):

Departure date, absolute or relative the same way as check-in. Defaults to check-in + 1 night.

## `windowDays` (type: `integer`):

Ask "any stay starting in the next N days" instead of one fixed stay. Set this together with **Nights per stay** below.

The window starts today, or at **Check-in** if you set one. Each day in it is a separate search of its own, **billed separately** — a 30-day window costs roughly 30× a single-date run — so keep it as short as the question allows and set **Max accommodations**, which is a budget across the whole sweep, not per day.

Capped at 60 days. Only applies to the *Search* and *Room pricing & availability* modes: the others return the same answer whatever the dates.

## `stayNights` (type: `integer`):

How long each stay in the window is. `1` asks for single nights — "the cheapest single night in the next 30 days". Ignored unless **Search a window of days** is set.

## `rooms` (type: `integer`):

Number of rooms to book for.

## `adults` (type: `integer`):

Number of adult guests.

## `childrenAges` (type: `array`):

Ages of the children in the party, one per line (e.g. 5 then 9). Booking prices family stays by child age (free cots, kids-stay-free rules), so set this for correct prices and to surface family rooms.

## `currency` (type: `string`):

Currency for displayed prices.

**Property's own currency** is the first choice and matches Booking's own picker: prices come back in whatever the property quotes in — MYR in Kuala Lumpur, THB in Bangkok — with no conversion. Every row states its own `currency`, so you always know which you got.

Booking cannot be *asked* for that, though: left to the property, the room-list request comes back with no rooms at all — on every property, not a few (measured 2026-10-29 on five properties in four markets). So this Actor reads each property's own code first and then asks for it explicitly, which is what the setting promises. Where a property's code cannot be read, its rates are skipped rather than mispriced and the run says so in its log, naming this input — pick an explicit code for that one.

Otherwise pick one of **147** currencies. That is every ISO-4217 code Booking honours on this surface, established by requesting a property once per code and reading back the currency it priced in, with bogus codes as the control.

The nine it refuses are ERN, FKP, GIP, KPW, SHP, SLE, TMT, VED and ZWG. They are refused outright rather than falling back silently, so you cannot ask for one by accident.

Booking's *app* only offers 49 of the 147 in its own menu, and its exchange-rate table lists 188 — including pre-euro currencies and gold. The 147 here is the set measured to actually price a room.

Every price in a row follows this setting, including the cancellation ladder and the prepayment schedule. **One documented exception:** `details.damageDeposit.amount` stays in the property's own currency, because a deposit is collected at the desk in local money — its `currency` says so.

## `language` (type: `string`):

Locale for localized text (names, facilities, reviews).

## `searchBy` (type: `string`):

Pick how to find properties, or leave on Auto. Ignored unless this run is a Search.

## `search` (type: `string`):

Place name (city, region, landmark) resolved via Booking's own autocomplete — the easiest way to start, e.g. "Amsterdam".

Autocomplete matches what Booking has a destination for, which is not always what you would guess: "Queens" resolves to Queenstown in New Zealand, and "Manhattan", "Newark" and "Long Island City" resolve to nothing at all. The run logs what it resolved to — check it. For a metro area Booking has no destination for, use mode 5 (custom area) instead.

Naming one specific hotel here also works: it is detected and fetched directly, the same as mode 2.

## `hotel_ids` (type: `array`):

Scrape specific properties by numeric Booking ID, one per line, e.g. 10004. **Set this and every other sub-mode is ignored.** These ids are also what the enrichment modes work on, so the same list re-checks rooms, reviews, scores or nearby places without searching.

This must be the internal numeric id, not a URL slug or a property name. To find it, open the hotel's Booking.com page, view the page source and search for `hotelId`.

Unlike a search, this always returns a row per id — including one saying why there is no price (sold out, minimum stay, closed, unknown id).

## `cityId` (type: `integer`):

Exact numeric destination id (ufi), e.g. -2140479 for Amsterdam. **This field takes a number**: typing "Amsterdam" here is rejected — use mode 1 for a name.

Only useful if you already have the id from a previous run's output.

## `regionId` (type: `integer`):

Exact numeric region id. **This field takes a number**, not a region name — use mode 1 for a name. Only useful if you already have the id.

## `latitude` (type: `number`):

Sub-mode **Point and radius**: search outward from one GPS point, nearest first — "within N km of this venue" rather than "in this city". Latitude and Longitude are required together; Radius is optional.

## `longitude` (type: `number`):

Longitude paired with Latitude.

## `radius` (type: `number`):

Only keep properties within this many kilometres of the point. **Applied after the results come back**, not by Booking — so a tight radius spends your Max accommodations budget on properties that are then discarded. Leave empty for nearest-first with no cut-off.

## `swLat` (type: `string`):

Sub-mode **Bounding box**: sweep an arbitrary rectangle instead of a destination. Use it for ground Booking has no single destination for — New York's city destination holds ~520 properties, while a box over the same area also reaches Jersey City, Hoboken and North Bergen.

**All four corners are required together.** A box costs far more requests per property than a destination search, so keep it tight and set Max accommodations.

**Coverage reporting differs.** A destination search can say how complete it was ("5,822 of 5,844 properties, 99.6%") because Booking reports a total; a box cannot, so the log reports whether every cell was fully reached instead of a percentage.

## `swLon` (type: `string`):

West edge of the search rectangle.

## `neLat` (type: `string`):

North edge of the search rectangle.

## `neLon` (type: `string`):

East edge of the search rectangle.

## `deepSearch` (type: `boolean`):

Booking's search stops returning new properties at roughly 1,000 per destination. With this on, the actor splits a large destination into geographic shards and merges the results, reaching several times more properties (measured: 1,104 -> 6,170 for Tokyo). Costs proportionally more requests, and each property is still returned - and billed - only once. Left unset, it turns on automatically when Maximum results is above 1,000.

## `accommodationTypes` (type: `array`):

Limit results to these property types. Leave empty for all types. Pick from the list here; via the API this field takes Booking’s numeric type ids as strings (e.g. "201" for Apartments). An id sent as a number, or a type name, is rejected before the run starts. To pass names case-insensitively ("Apartments", "villa"), or ids as numbers, use the snake_case spelling "accommodation_types" instead. See the README for the full id → name table.

## `minReviewScore` (type: `integer`):

Only return properties with at least this guest review score (1–10). Applied client-side.

## `minPrice` (type: `integer`):

Only return properties at or above this total stay price, in the selected currency. Applied client-side.

This runs on the price set by **Price basis**: by default Booking's displayed price, which is net of taxes and fees that are charged on top. On a property with excluded charges the all-in cost is higher, so a budget set here can be overshot — set Price basis to *all-in* to filter on what the guest actually pays.

## `maxPrice` (type: `integer`):

Only return properties at or below this total stay price, in the selected currency. Applied client-side.

This runs on the price set by **Price basis**: by default Booking's displayed price, which is net of taxes and fees that are charged on top. On a property with excluded charges the all-in cost is higher, so a budget set here can be overshot — set Price basis to *all-in* to filter on what the guest actually pays.

## `priceBasis` (type: `string`):

Which price <b>Minimum price</b>, <b>Maximum price</b> and <b>Cheapest first</b> are measured against.

<b>Booking's displayed price</b> (the default) is the figure Booking shows in its own result list. It is net of any tax or fee charged on top — city tax, VAT, a resort fee — so the stay can cost more than the number you filtered on. Pick this to match what you see on the website.

<b>All-in price</b> is the displayed price plus every charge Booking reports as excluded, i.e. what the guest pays. Pick this when a budget or a "cheapest" ranking has to be true of the final bill. Each row carries both figures either way, as <code>price.total</code> and <code>price.total_with_charges</code>.

## `sortBy` (type: `string`):

How Booking orders the results. **Cheapest first** is the default. These are Booking's own seven orderings - the same menu its app shows - and the order is applied by Booking itself, so a small <b>Max items</b> still gives you the real cheapest (or best-reviewed) properties for the whole destination rather than the cheapest of the first page.

<b>Entire homes & apartments first</b> promotes apartments and aparthotels above hotels, which is Booking's own "upsort" ordering rather than a filter - hotels still appear, lower down. Use the property-type filter if you want only homes.

**Most expensive first was withdrawn on 2026-10-21.** Booking has no such ordering on this surface, so it had to be ranked here over an over-fetched candidate set, which made it silently approximate whenever the over-fetch did not reach far enough. Asking for it now fails with a message instead. Sort the dataset yourself after the run - every row carries its own price.

**Cheapest first** orders by the price set by <b>Price basis</b>. With the default (Booking's displayed price) the order is Booking's own; with <i>all-in</i> the ranking is done here, over an over-fetched candidate set, because Booking has no all-in sorter.

## `sortDirection` (type: `string`):

**Mostly ignored — the direction is part of <b>Sort results by</b> now.** Pick "Cheapest first", "Most stars first" and so on. This field is kept so existing saved tasks and schedules keep validating.

<b>"Descending" is refused, not ignored, on every order Booking only provides one way</b> — cheapest, most/fewest stars, nearest, homes-first. The run stops with a message naming the alternative instead of quietly serving a different order under your label. Only <i>Best reviewed</i> genuinely has both directions.

To get the reverse of an order Booking does not offer, sort the dataset after the run: every row carries its own price, review score and star rating.

## `includeRooms` (type: `boolean`):

Add both room sections: the per-rate `products` list (board, cancellation, deals, price) AND the distinct room-type `rooms` list (name, occupancy, availability, lowest price). Costs one extra request per property in search mode.

## `onlyFittingProducts` (type: `boolean`):

When rooms are included, keep only rates Booking would actually sell to your party (its `is_block_fit` flag) in `products`, and compute `rooms[].minPrice` / `productCount` from those. Turn off to get every rate of every occupancy (each still carries `fitsParty`).

## `includeDetails` (type: `boolean`):

Add a `details` object per property: name, type, star rating, review score/count, location, main photos, named facilities, languages spoken and address. Costs one extra request per property in search mode.

## `includeSurroundings` (type: `boolean`):

Add nearby landmarks, public transport, dining and airports with distances, under "details.surroundings". Costs one extra API request per property and is billed as its own "Nearby places" event. Off by default: until 1.0.50 this was fetched automatically with "Include details" and not charged.

## `includeReviews` (type: `boolean`):

Add a `reviews` array per property (author, score, title, pros/cons, date). Costs one request per 10 reviews per property.

## `maxReviews` (type: `integer`):

How many reviews to fetch per property (only when "Include reviews" is on). Capped by the property's total review count.

## `includeReviewScores` (type: `boolean`):

Add per-category review subscores (staff, cleanliness, location, value, comfort, facilities, wifi) and the aggregated review summary, under "reviewSubscores" and "reviewSummary". Costs one extra API request per property regardless of how many reviews you fetch, and is billed as its own "Review scores" event. Off by default: until 1.0.50 this rode along free with "Include reviews".

## `includeExtraCharges` (type: `boolean`):

Add the `extra_charges` price breakdown (taxes/fees) to each result and product.

## `hotelIdsRooms` (type: `array`):

The properties to run **Room pricing & availability** on, one numeric Booking ID per line, e.g. 10004. This must be the internal numeric id, not a URL slug or a property name: open the hotel's Booking.com page, view source and search for `hotelId`.

## `roomsOnlyFitting` (type: `boolean`):

Keep only rates Booking would actually sell to the party in **Stay details** below. Turn off for every rate of every occupancy. This is the Rooms-mode copy; it overrides the shared *Only rates that fit the party* field.

## `roomsExtraCharges` (type: `boolean`):

Break each tax and fee out of the rate instead of a blended total. This is the Rooms-mode copy; it overrides the shared *Include extra-charge breakdown* field.

## `productIds` (type: `array`):

The individual rates to re-check, one product ID per line, e.g. `25768602_431556758_2_0_0`. These are the `products[].id` values from an earlier *Search* or *Room pricing & availability* run.

The property is worked out from the ID, so nothing else has to identify it — but a rate exists only for a given stay and party, so **Stay details** still applies and should match the run the IDs came from. One row comes back per ID you ask for, including rates that are no longer on sale.

## `hotelIdsReviews` (type: `array`):

The properties to run **Guest reviews** on, one numeric Booking ID per line, e.g. 10004. This must be the internal numeric id, not a URL slug or a property name: open the hotel's Booking.com page, view source and search for `hotelId`.

## `ufi` (type: `string`):

Booking's reviews endpoint refuses to return anything without the destination's `ufi`, so the reviews mode needs one. Take it from an earlier search run's output and paste it here to skip a lookup; leave it empty and one extra request per property derives it. A ufi identifies a destination, not a property, so one value covers every property in that destination.

## `countryCode` (type: `string`):

ISO 2-letter code for the property's country, paired with the ufi above. Optional — Booking tolerates it being empty.

## `reviewsMax` (type: `integer`):

Capped by the property's own review count. This is the Reviews-mode copy; it overrides the shared *Max reviews per property* field.

## `hotelIdsScores` (type: `array`):

The properties to run **Review score summary** on, one numeric Booking ID per line, e.g. 10004. This must be the internal numeric id, not a URL slug or a property name: open the hotel's Booking.com page, view source and search for `hotelId`.

## `hotelIdsSurroundings` (type: `array`):

The properties to run **Nearby places** on, one numeric Booking ID per line, e.g. 10004. This must be the internal numeric id, not a URL slug or a property name: open the hotel's Booking.com page, view source and search for `hotelId`.

## `hotelIdsDetails` (type: `array`):

The properties to run **Static property details** on, one numeric Booking ID per line, e.g. 10004. This must be the internal numeric id, not a URL slug or a property name: open the hotel's Booking.com page, view source and search for `hotelId`.

## `outputFields` (type: `array`):

Leave empty for the full record. Selecting Nearby places without Static details keeps a `details` object holding only `surroundings`.

## `maxItems` (type: `integer`):

Maximum number of accommodations to return.

Above 1,000 this turns Deep search on by itself, because Booking's search stops returning new properties at roughly that point.

**This limit is the usual reason a run returns less than the destination holds.** Every run writes a `RUN_SUMMARY` record to its key-value store saying how many properties were available, how many were delivered, and which limit stopped it — so you can tell a complete result from a truncated one without guessing. The run's status message says the same thing in words when a run was cut short.

## `maxConcurrency` (type: `integer`):

Maximum parallel requests for product/detail fetches.

## `units` (type: `string`):

Unit system for distances - distance from the city centre and the nearby landmarks, transport and airports returned with includeDetails. Booking localises distances by language (metres for German, feet for US English); this normalises them to the system you pick.

## `exportExcel` (type: `boolean`):

Also write an Excel workbook to this run's storage, with a sheet per section (Properties, and Rates/Rooms/Reviews when those are included) instead of nested JSON in a cell. Download it from the run's **Storage** tab. Does not change the dataset or what you are charged.

**In enrichment modes only that mode's sheet fills**: Rates and Rooms for *Room pricing*, Reviews for *Guest reviews*. The Properties sheet holds the id alone, and nearby places, review scores and static details have no sheet — they reach you through the dataset instead.

## `excelFileName` (type: `string`):

Name for the workbook in the key-value store.

## `exportKml` (type: `boolean`):

Also write a KML file with one placemark per property, ready to open in Google Earth, Google My Maps or QGIS. Each pin carries the price, review score, address and a link to the property, plus a photo when **Include details** is also on. Properties without coordinates are skipped.

**Only produces pins in Search mode.** The enrichment modes return rows without coordinates, so a map built from them has 0 placemarks. For a map of properties you already have ids for, use Search with the *Specific properties* sub-mode.

## `kmlFileName` (type: `string`):

Name for the KML file in the key-value store.

## `kmlColorBy` (type: `string`):

Shade the placemarks so the map reads at a glance. **Price** uses this run's own quartiles (cheapest green, dearest red); **review score** uses fixed bands (9+ green, 8-9 amber, 7-8 orange, below 7 red).

## `proxyConfiguration` (type: `object`):

Proxy settings. Apify Proxy recommended.

## `attractionsQuery` (type: `string`):

Where to look, as free text — a city (`Amsterdam`), a landmark, or a product name. Booking resolves it server-side. Give this, or **Destination ufi**, or a latitude/longitude pair.

## `attractionsUfi` (type: `integer`):

Booking's own numeric destination id — the same `ufi` the property search uses, e.g. `-2140479` for Amsterdam. More precise than free text when you already have it.

## `attractionsLatitude` (type: `string`):

Search around a point instead of a destination. Needs **Longitude** too.

## `attractionsLongitude` (type: `string`):

Needs **Latitude** too.

## `attractionsRadiusKm` (type: `integer`):

Only with a latitude/longitude pair. Caps how far from the point results may be.

## `attractionsSortBy` (type: `string`):

Sorted server-side by Booking. Leave on the default unless you need a specific order — an unrecognised value is ignored upstream rather than refused.

## `attractionsMinRating` (type: `string`):

Keep only attractions rated at or above this many stars. Booking rates attractions out of <b>5</b>, not out of 10 like hotels — so "4.5" here is strict, and a 1-10 score such as 8 matches nothing.

## `attractionsTypes` (type: `array`):

Pick one or more of Booking's own product categories. Filtered by Booking, not client-side. These seven are the whole taxonomy — the same set and the same ids came back for Amsterdam, Rome and London, so they are global rather than per-destination. Leave empty for everything.

## `attractionsLabels` (type: `array`):

Booking exposes exactly one label filter on this surface today. Kept as a list because the upstream field takes several.

## `attractionsIncludeUnavailable` (type: `boolean`):

Off by default, so you only get what Booking will actually sell right now. Turn on to see the full catalogue including sold-out or out-of-season products.

## `attractionsPageSize` (type: `integer`):

How many products to ask for per request while paging. Does not change the total — that is **Max accommodations**.

## `attractionIds` (type: `array`):

The attractions to look up, one id per line, e.g. `PRz8B1W8aYsI`. Used by **Attraction details** and **Attraction availability**. You get these ids from a run in mode **Attractions** (the `attractionId` field).

## `attractionsStartDate` (type: `string`):

First date of the availability window. Used by **Attraction availability**, and also narrows **Attractions** to products bookable in the window. Accepts a relative value like "2 weeks".

## `attractionsEndDate` (type: `string`):

Last date of the availability window, absolute or relative. Booking returns no product at all for one with nothing bookable inside the window — the run reports that as `bookableInWindow: false` rather than dropping it.

## `attractionsIncludeTimeslots` (type: `boolean`):

Off by default. Adds the bookable timeslots for each available date — start time, ticket type, tickets left and price. Booking serves timeslots **one date per request**, so this costs one extra request per available date; **Timeslot days** caps how many.

## `attractionsTimeslotDays` (type: `integer`):

How many available dates to fetch timeslots for, at one request each. Only used with **Also fetch timeslots**. The run logs how many dates it skipped.

## Actor input object example

```json
{
  "mode": "search",
  "rooms": 1,
  "adults": 2,
  "childrenAges": [],
  "currency": "USD",
  "language": "en-us",
  "searchBy": "auto",
  "search": "Amsterdam",
  "hotel_ids": [],
  "accommodationTypes": [],
  "priceBasis": "displayed",
  "sortBy": "price",
  "sortDirection": "ascending",
  "includeRooms": false,
  "onlyFittingProducts": true,
  "includeDetails": false,
  "includeSurroundings": false,
  "includeReviews": false,
  "maxReviews": 10,
  "includeReviewScores": false,
  "includeExtraCharges": true,
  "roomsOnlyFitting": true,
  "roomsExtraCharges": true,
  "reviewsMax": 10,
  "outputFields": [],
  "maxItems": 100,
  "maxConcurrency": 10,
  "units": "metric",
  "exportExcel": false,
  "excelFileName": "OUTPUT.xlsx",
  "exportKml": false,
  "kmlFileName": "OUTPUT.kml",
  "kmlColorBy": "none",
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "attractionsSortBy": "none",
  "attractionsIncludeUnavailable": false,
  "attractionsPageSize": 30,
  "attractionsIncludeTimeslots": false,
  "attractionsTimeslotDays": 3
}
```

# Actor output Schema

## `accommodations` (type: `string`):

Everything this run delivered: one item per property in the search, rooms, details, surroundings, reviews and reviewScores modes, one per attraction in the attractions modes, and one per watched product id in rates. Each kind has its own view in the Output tab.

## `runSummary` (type: `string`):

How complete this run was: properties available, delivered and requested, whether it was truncated and by which limit, and any requests that gave up after exhausted retries. Written on every run, so a scheduled task can tell a stable destination from a silently truncated one.

## `datasetView` (type: `string`):

Open the dataset in the Apify Console.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "mode": "search",
    "rooms": 1,
    "adults": 2,
    "childrenAges": [],
    "searchBy": "auto",
    "search": "Amsterdam",
    "hotel_ids": [],
    "accommodationTypes": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("rl1987/booking-api-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "mode": "search",
    "rooms": 1,
    "adults": 2,
    "childrenAges": [],
    "searchBy": "auto",
    "search": "Amsterdam",
    "hotel_ids": [],
    "accommodationTypes": [],
}

# Run the Actor and wait for it to finish
run = client.actor("rl1987/booking-api-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "mode": "search",
  "rooms": 1,
  "adults": 2,
  "childrenAges": [],
  "searchBy": "auto",
  "search": "Amsterdam",
  "hotel_ids": [],
  "accommodationTypes": []
}' |
apify call rl1987/booking-api-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,rl1987/booking-api-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/hOb6eSGvMQIbf6z1X/builds/qRdpy9EbilOPYDqlp/openapi.json
