# Booking.com Hotel Price Calendar Scraper (`aleksandrbooking/booking-rate-calendar`) Actor

Track a hotel and its comp set on Booking.com across every check-in date: nightly rate, stay total, pre-discount price, taxes, room type, cancellation policy, rooms left. Sold-out dates flagged; currency, language and tax regime verified in every response. Not affiliated with Booking.com.

- **URL**: https://apify.com/aleksandrbooking/booking-rate-calendar.md
- **Developed by:** [Aleksandr Kobtsev](https://apify.com/aleksandrbooking) (community)
- **Categories:** Travel, Real estate, E-commerce
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.75 / 1,000 hotel dates

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

![Booking.com Hotel Price Calendar Scraper](https://api.apify.com/v2/key-value-stores/iegwMa0DKgiYG884T/records/banner.png)

Track your own property and your comp set on Booking.com across every check-in date in a
range, and get prices and availability by date: nightly rate, total for the stay,
pre-discount price, taxes, room type, cancellation and prepayment policy, and how many
rooms are left. One dataset row per hotel and per date, up to 365 days ahead, in 12
currencies and 7 languages.

You bring the hotel page URLs. There is no login, no Booking.com account and no partner
API involved, and this Actor does not search for hotels: it prices the ones you name.
Run it once, put it on a schedule, or call it from the API, and forward the result to
Google Sheets, Slack, Make, Zapier or an MCP client.

### Who is this Booking.com scraper for?

| You are | What you run | What it costs |
|---|---|---|
| A revenue manager watching a comp set | 10 hotel URLs, 90 dates, daily | about $0.98 per run |
| A short-let or small hotel owner | your own property, 60 dates, daily | about $7.82 per month |
| Building a rate-shopping or BI product | any number of URLs through the API | $3.00 per 1,000 hotel-dates |
| Researching travel prices | one pull, 365 dates | $1.18 |

If you need to find hotels rather than price known ones, this is the wrong tool. Start
from a Booking.com search scraper, then feed the URLs it returns into this one.

### What makes this different from other Booking.com scrapers?

Most Booking.com Actors answer "which hotels are in this city, and what is the headline
price today". They bill one row per property. This one answers "what does this property
cost on the 10th, the 11th, the 12th of next month, and what were the terms", and it
bills one row per hotel and date, with every rate on that date inside it.

- **It verifies before it bills.** Booking.com switches currency and language to match
  the visitor's address. Every response is checked against what you asked for: the
  currency on every single rate, the language, and the check-in date the page echoes
  back. A page that answers for the wrong dates is rejected and never charged.
- **It tells sold-out from broken.** Four page states are recognised on real pages:
  rooms with prices, genuinely sold out, the decoy page Booking.com serves to suspected
  bots, and a delisted property whose URL now redirects to a city page. Only the first
  two reach your dataset. The decoy is retried on a fresh session, the delisted URL is
  named in the run output, and neither is billed.
- **A run that collects nothing fails.** It does not finish green with an empty dataset,
  which is the failure that costs you a day before you notice it.
- **It is watched.** A scheduled job re-parses live Booking.com pages against a recorded
  baseline three times a day and alerts on a markup change, so a break surfaces in hours
  and not in your Issues tab a week later.
- **It survives layout changes without waiting for a release.** Every CSS selector can be
  overridden from the input, for example `{"price": ".new-price-class"}`.

### What data can you extract from a Booking.com hotel page?

One row per hotel and check-in date. The flat `cheapest*` fields repeat the cheapest rate
so a CSV or Excel export stays readable; `offers` holds every rate the page showed.

| Row field | What it is |
|---|---|
| **hotelUrl**, **hotelId**, **hotelName** | The property, as Booking.com identifies it. `hotelId` is the stable join key. |
| **checkIn**, **checkOut**, **nights** | The stay this row priced. |
| **adults**, **rooms**, **childrenAges** | The occupancy it was priced for. Change it and the prices change. |
| **currency**, **language** | Confirmed against the response, not just requested. |
| **soldOut** | `true` when the property has no availability for that date. A real answer, and a billed row. |
| **minPricePerNight** | The headline number: cheapest rate divided by `nights`. |
| **pricesIncludeTaxes** | Whether these prices already contain local tax. See the section below. |
| **offersCount** | How many rates the page offered. Typically 20 to 48. |
| **cheapestRoomType**, **cheapestTotalPrice**, **cheapestStrikeoutPrice** | The cheapest rate, flattened. |
| **cheapestTaxesAndCharges**, **cheapestCancellationPolicy**, **cheapestPrepaymentPolicy**, **cheapestRoomsLeft** | Its terms, flattened. |
| **offers** | Every rate on the date. Fields below. |
| **scrapedAt** | When it was collected, ISO-8601 UTC. |

Each entry in **offers** carries **roomType**, **totalPrice**, **pricePerNight**,
**strikeoutPrice**, **currency**, **taxesAndCharges**, **cancellationPolicy**,
**prepaymentPolicy**, **occupancy** and **roomsLeft**.

```json
{
  "hotelUrl": "https://www.booking.com/hotel/ge/radisson-blu-batumi.html",
  "hotelId": "320991",
  "hotelName": "Radisson Blu Hotel Batumi",
  "checkIn": "2026-11-10",
  "checkOut": "2026-11-12",
  "nights": 2,
  "adults": 2,
  "rooms": 1,
  "childrenAges": [],
  "currency": "USD",
  "language": "en-gb",
  "soldOut": false,
  "minPricePerNight": 279.5,
  "pricesIncludeTaxes": true,
  "offersCount": 20,
  "cheapestRoomType": "Premium Room",
  "cheapestTotalPrice": 559,
  "cheapestStrikeoutPrice": 621,
  "cheapestTaxesAndCharges": "Includes taxes and charges",
  "cheapestCancellationPolicy": "Non-refundable",
  "cheapestPrepaymentPolicy": "Pay online",
  "cheapestRoomsLeft": null,
  "scrapedAt": "2026-11-01T09:14:02.113Z",
  "offers": [
    {
      "roomType": "Premium Room",
      "totalPrice": 559,
      "strikeoutPrice": 621,
      "pricePerNight": 279.5,
      "currency": "USD",
      "taxesAndCharges": "Includes taxes and charges",
      "cancellationPolicy": "Non-refundable",
      "prepaymentPolicy": "Pay online",
      "occupancy": "Max persons: 2",
      "roomsLeft": null
    }
  ]
}
```

A sold-out date comes back with `soldOut: true`, `offersCount: 0` and
`minPricePerNight: null`, so a gap in your calendar means "no rooms", never "the scrape
missed a day".

**What to do with it.** Put the run on a daily schedule, send the dataset to Sheets or
BigQuery, and key rows on `hotelId` plus `checkIn`. Two columns then answer the morning
question on their own: a change in `minPricePerNight` is a rate move, and a change in
`soldOut` is your comp set filling up. `scrapedAt` keeps the history straight.

### Do Booking.com prices include tax? 175 USD or 207 USD for the same room

Booking.com quotes a rate with or without local tax depending on the country it infers
from the address the request came from. The gap is the whole VAT. Measured on one Georgian
property, same date, same room, two runs minutes apart: 175 USD from one exit address and
207 USD from another, 18 percent apart, with 48 rates on the page both times.

Both are genuine Booking.com prices. The damage comes from mixing them without saying so,
which is why every row states `pricesIncludeTaxes`.

On the default Apify datacenter proxies the flag reads `false`, and it reads `false`
consistently, so a calendar built on them compares correctly with itself. If you need the
tax-inclusive quote, run through your own proxies in the country you want to buy from.
Booking.com takes the regime from the address and ignores the URL parameters that look
like they should control it. That was verified rather than assumed: `sb_price_type=total`
and `cc1` both changed nothing.

### How much does it cost to scrape Booking.com prices?

**One billed row is one hotel on one check-in date, with every rate that property offered
on it.** That is the whole unit. Thirty dates are thirty rows on every property, always,
so the bill is predictable before you start.

| Event | Price | When |
|---|---|---|
| Hotel date | $0.003 | Per row that reaches the dataset |
| Actor start | $0.02 per GB | Once per run. The Actor needs 4 GB, so $0.08 |
| Result | $0.00001 | Per stored item, a fraction of a cent on any real job |

What that means for real jobs:

| Job | Rows | Total |
|---|---|---|
| One hotel, a month ahead | 30 | **$0.17** |
| One hotel, a quarter ahead | 90 | **$0.35** |
| One hotel, a year ahead | 365 | **$1.18** |
| Ten competitors, a quarter ahead | 900 | **$2.79** |
| One property, 60-date window, refreshed daily for a month | 1,800 | **$7.82** |

The start charge is flat, so it dominates a tiny run and disappears on a large one: one
date works out near $0.083 per row, thirty dates near $0.0057. Put many hotels into one
run instead of starting a run per hotel.

Sold-out dates are charged, because "no rooms on that date" is a real answer and the
reason people monitor availability. Nothing else is: not a blocked or challenged request,
not a page in the wrong currency or language, and not a page the parser could not read.
The $0.08 start charge is the exception, since it is taken before any of that is known.

Set **Maximum total charge** on the run to cap spending. The Actor stops cleanly at the
budget and lists the dates it did not reach in the `FAILED_CHUNKS` key-value store record,
so you can finish them later without paying twice for what you already have.

**How this compares.** Actors that return every rate per date bill per rate, not per date.
For one hotel over 30 dates at roughly 34 rates a date, that is about 1,020 billable units
against this Actor's 30. Priced on the Store today, the same job comes to roughly $3.06 and
$4.08 at two of them, against $0.17 here. Actors that look cheaper per 1,000 are selling a
search-listing row: one property, one headline price, no room types, no policies.

### How to scrape Booking.com hotel prices by date

1. Open the hotel on Booking.com and copy the page URL. Anything like
   `https://www.booking.com/hotel/ge/radisson-blu-batumi.html` works. Dates, language and
   tracking parameters already in the link are ignored.
2. Paste one or more URLs into **Hotel page URLs**.
3. Choose the first check-in date, how many dates to cover, nights per stay, guests,
   currency and language.
4. Run it. Download the dataset as JSON, CSV or Excel, or read it through the API.
5. For a price history, put the run on a daily schedule and keep every run's dataset.

| Field | Default | What it does |
|---|---|---|
| `hotelUrls` | required | Booking.com hotel page links, one per line. |
| `startDate` | tomorrow | First check-in date, `YYYY-MM-DD` (UTC). Today is allowed; a past date is moved to today. |
| `days` | 30 | Number of consecutive check-in dates, **per hotel**. Each hotel and date pair is one billable row, so 5 URLs over 30 dates is 150 rows. |
| `nights` | 1 | Length of stay for every check-in date. |
| `adults` | 2 | Adult guests. Occupancy changes prices, so compare like for like. |
| `childrenAges` | `[]` | Age of each child, e.g. `[5, 9]`. Booking.com prices children by age, so a count alone is not enough. |
| `rooms` | 1 | Rooms to price. |
| `currency` | `USD` | Verified in every response. A wrong currency means the date is rejected and not charged. |
| `language` | `en-gb` | Language of room names and policies. Also verified. |
| `datesConcurrency` | 3 | Dates fetched in parallel per browser tab. |
| `datesPerSession` | 100 | Dates taken from one browser session before a fresh one starts. Measured stable at 100. |
| `proxy` | Apify datacenter | Measured on Booking.com: 10 of 10 dates, zero rejections. Apify residential groups are refused in the first seconds, before anything is charged. For a specific country, or a residential address, choose Custom proxies and supply your own. The country also decides `pricesIncludeTaxes`. |
| `dryRun` | `false` | Owner-only switch used by the maintainer's own monitoring. It is ignored for everyone else. |
| `selectorOverrides` | `{}` | Emergency hatch: map a selector name to a CSS selector, e.g. `{"taxes": ".prd-taxes-new"}`. Use it when the run log says a selector stopped matching. |

### How fast is it, and what does it not do?

Measured on the default Apify datacenter proxy: 14 dates for one hotel in 68 seconds, 30
dates in 250 seconds. A full year is therefore roughly 30 to 50 minutes per hotel, and ten
hotels over a year is several hours. The run timeout ships at 3 hours, so raise it in the
run options before a job that large and size it from the slow end of that range.

- Prices are what Booking.com showed for that occupancy, currency and language at scrape
  time. They move constantly. Scrape on a schedule if you need a trend.
- `totalPrice` is the price for the whole stay as displayed. `pricePerNight` is derived
  from it by dividing by `nights`.
- `taxesAndCharges`, `strikeoutPrice` and `occupancy` are `null` when Booking.com does not
  print them. The tax line in particular depends on the point of sale, as described above.
- `roomsLeft` is how many rooms the booking form lets you pick, and that list stops at
  nine. At the ceiling the field is `null`, because nine would read as an exact count and
  it is not one. Measured: half the rates on a live page sit at the ceiling.
- `datesPerSession` has a floor of 10. Below it the hotel page is re-warmed for almost
  every date, which costs about eleven times the traffic and returns the same data.
- English (US) is not offered. With the US locale Booking.com ignores the requested
  currency and prices the property in its local one, which would make every row unusable.
  Use English (UK). The room descriptions are identical.
- Member-only Genius discounts need a logged-in session and are not included.
- It does not collect reviews, photos, guest names or any personal data.

### FAQ: scraping Booking.com hotel prices

**Can I scrape several hotels at once?** Yes, one URL per line. Each hotel gets its own
browser session.

**How far ahead can it price?** Up to 365 check-in dates per hotel per run.

**Do I need a Booking.com account or an API key?** No. It reads publicly visible pages.

**What if Booking.com changes the page?** A scheduled watchdog re-parses live pages
against a recorded baseline three times a day, so the break is usually known before you
hit it. Parsing errors are reported in the log and never billed. You can also put the new
selector into `selectorOverrides` and run again without waiting for a release.

**What if a hotel URL is dead?** Delisted properties redirect to a city page. That is
detected on the first attempt, the URL is skipped without retries and named in the
`FAILED_CHUNKS` key-value store record.

**Does it work for apartments and hostels?** Yes. Prices and policies are verified on
hotels, hostels and guest houses in Georgia, Czechia and India. On apartments the parser
is verified on a sold-out date in Spain, which proves the availability path but not the
rate table. Booking.com serves the same room markup for all of them, so apartments are
expected to work. If one does not, open an issue with the URL.

**Can I get a currency that is not in the list?** Not yet. The twelve supported currencies
are the ones whose price notation on Booking.com has been verified, which is what makes a
wrong-currency response catchable. Ask in Issues if you need another.

**Is it legal to scrape Booking.com?** This is an unofficial tool, not affiliated with,
endorsed by or connected to Booking.com or Booking Holdings Inc. It reads publicly visible
pages and collects no personal data. Booking.com's terms of service restrict automated
access, and you are responsible for making sure your use complies with those terms, with
the law in your jurisdiction and with the site's `robots.txt`. See
[Apify's stance on web scraping legality](https://blog.apify.com/is-web-scraping-legal/).

### Found a problem?

Open an issue on the Issues tab with the hotel URL and the run ID. Breakages in Booking.com
markup are the expected failure mode of any scraper, and this one is instrumented to catch
them early. Reports get read.

# Actor input Schema

## `hotelUrls` (type: `array`):

Links to Booking.com hotel pages, one per line, e.g. https://www.booking.com/hotel/ge/radisson-blu-batumi.html. Any dates, language or tracking parameters already in the link are ignored; the actor sets its own.

## `startDate` (type: `string`):

YYYY-MM-DD (UTC). Leave empty to start from tomorrow. Today is allowed; a date in the past is moved to today.

## `days` (type: `integer`):

Number of consecutive check-in dates, per hotel. Each hotel and date pair is one billable row, so 5 hotel URLs over 30 dates is 150 rows. The form starts at 14 so a first run finishes quickly; raise it for a fuller calendar. Longer runs also cost less per row, because the flat start charge of the run is spread over more rows.

## `nights` (type: `integer`):

Length of stay used for every check-in date. Booking shows a total for the stay; the actor also reports the price per night.

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

Number of adult guests. Occupancy changes the price, so compare like for like.

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

Age of each child as a JSON list, e.g. \[5, 9]. Booking prices children by age, so a plain count is not enough. Leave empty for adults only.

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

Number of rooms to price for the same stay.

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

Prices are verified to actually come back in this currency. If Booking.com returns a different one, the date is rejected and not charged. Only currencies whose price format has been verified are offered. Ask in Issues if you need another.

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

Language of room names and policies. Verified in every response. English (US) is deliberately absent: with lang=en-us Booking.com ignores the requested currency and prices the property in its local one, so every date would be rejected. Use English (UK); the room descriptions are the same.

## `datesConcurrency` (type: `integer`):

How many dates one browser tab requests at the same time. Higher is faster but uses more memory: each response expands to 4–5 MB in RAM. Keep 3 on 4 GB.

## `datesPerSession` (type: `integer`):

How many dates to take from one warmed-up browser tab before starting a fresh one with a new proxy. Measured: 100 dates in a single session with zero rejections. The minimum is 10. Below it the hotel page is re-warmed for almost every date, which costs about eleven times the traffic and returns the same data.

## `proxy` (type: `object`):

Apify datacenter proxies are the default and they are what this Actor is priced on: measured on Booking.com, 10 of 10 dates collected with zero rejections. Apify residential groups are not available here; they cost about twenty times more per row than the Actor earns, so a run that asked for one is refused in the first seconds and nothing is charged. If you need a specific country, or a residential address, choose Custom proxies and supply your own: the Actor uses them exactly as given. The country matters for more than blocking. Booking quotes a rate with or without local tax depending on where it thinks you are, so the exit address decides what pricesIncludeTaxes says in every row.

## `dryRun` (type: `boolean`):

Produce data without charging events. Honored only for runs started by the actor owner (used for the owner's own monitoring); ignored for everyone else.

## `selectorOverrides` (type: `object`):

Emergency hatch for the day Booking.com changes its markup and a new Actor version is not out yet. Maps a selector name to a CSS selector, e.g. {"taxes": ".prd-taxes-new"}. Names that are not recognised and values that do not look like a CSS selector are ignored and logged. Leave empty unless the run log tells you a selector stopped matching.

## Actor input object example

```json
{
  "hotelUrls": [
    "https://www.booking.com/hotel/ge/radisson-blu-batumi.en-gb.html"
  ],
  "days": 14,
  "nights": 1,
  "adults": 2,
  "childrenAges": [],
  "rooms": 1,
  "currency": "USD",
  "language": "en-gb",
  "datesConcurrency": 3,
  "datesPerSession": 100,
  "proxy": {
    "useApifyProxy": true
  },
  "dryRun": false,
  "selectorOverrides": {}
}
```

# Actor output Schema

## `rateCalendar` (type: `string`):

No description

## `failedChunks` (type: `string`):

No description

# 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 = {
    "hotelUrls": [
        "https://www.booking.com/hotel/ge/radisson-blu-batumi.en-gb.html"
    ],
    "days": 14
};

// Run the Actor and wait for it to finish
const run = await client.actor("aleksandrbooking/booking-rate-calendar").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 = {
    "hotelUrls": ["https://www.booking.com/hotel/ge/radisson-blu-batumi.en-gb.html"],
    "days": 14,
}

# Run the Actor and wait for it to finish
run = client.actor("aleksandrbooking/booking-rate-calendar").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 '{
  "hotelUrls": [
    "https://www.booking.com/hotel/ge/radisson-blu-batumi.en-gb.html"
  ],
  "days": 14
}' |
apify call aleksandrbooking/booking-rate-calendar --silent --output-dataset

```

## MCP server setup

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

```

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/49K1CC6ksD3DumcWq/builds/Qx7GMhJQHLFeftDXm/openapi.json
