# Booking.com Scraper With Property Contact Leads (`simpleapi/booking-scraper`) Actor

🏨 BookingScraper extracts real-time hotel and apartment data from Booking.com — prices, availability, ratings, reviews, amenities, photos & location. 🔎 Ideal for market research, price monitoring, travel SEO & competitor analysis. ⚡ Fast, scalable API + CSV/JSON export.

- **URL**: https://apify.com/simpleapi/booking-scraper.md
- **Developed by:** [SimpleAPI](https://apify.com/simpleapi) (community)
- **Categories:** Travel, Developer tools, E-commerce
- **Stats:** 16 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.99 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

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 Scraper — Hotels, Prices and Property Contact Leads

Booking.com Scraper With Property Contact Leads scrapes hotel, apartment, hostel, villa and other property listings from Booking.com and returns typed JSON — plus a flattened contact lead per property: `contactEmail`, `contactPhone`, `contactWebsite`, `contactCompany` and `contactName`, alongside `price`, `rating`, `stars`, `address` and `rooms`. It's built for hospitality sales teams, lead-generation agencies and travel-tech researchers who need a property list they can actually contact, not just browse. Paste a destination or Booking.com URLs and every result streams in as a ready-to-use lead row.

### What is Booking.com Scraper With Property Contact Leads?

It's an Apify Actor that searches Booking.com by destination (or takes direct Booking.com URLs), scrapes each property's public detail page, and shapes the result into two things at once: a full property record (price, rating, rooms, facilities, address, images, policies) and a flattened contact-lead block (email, phone, website, company, contact name). Output is one JSON row per property, exportable as JSON, CSV, Excel, XML or HTML table.

No Booking.com account or login is used anywhere in the run — every field comes from the property's public page or a public web search.

- Discover properties by **destination search** (`search`) or by pasting **Booking.com URLs** directly (`startUrls`) — hotel detail pages or search-results pages both work
- Every row carries a **contact-lead block** built from the property's on-page trader/business details, best-effort backfilled by a keyless web search when that block is missing
- Filter search results by **property type, star rating, minimum guest rating, price range and sort order** before you scrape a single detail page
- Set **check-in/check-out dates, room and guest counts, and date flexibility** to pull live, stay-specific pricing instead of a generic rate
- Narrow the output to **contactsOnly** rows or to a specific **email-domain allow-list** so the export is a clean lead list, not a full property dump
- Export straight from the Apify Console as JSON, CSV, Excel, XML or HTML table, or read the dataset through the Apify API

### What Booking.com property and contact data is publicly available to scrape?

Booking.com renders full property details — price, rating, rooms, facilities, images, address — to any anonymous visitor. Direct contact information is a different story: Booking.com deliberately keeps most property owners routed through its own messaging system, so email and phone are only reliably public for a subset of listings.

| Data category | Availability |
| ----- | ----- |
| Property name, price, rating, stars, reviews | ✅ Public on the property page |
| Address, coordinates, rooms, facilities, images, policies | ✅ Public on the property page |
| Trader/business email, phone, company name (EU-regulated and business listings) | ✅ Public — shown in the property's own legal/trader block |
| Property email, phone, website for listings without a trader block | ❌ Not on the page — recovered only via best-effort keyless web search, when findable |
| Guest names, reservations, payment details | ❌ Never collected — not on the public listing page and out of scope for this Actor |

Booking.com Scraper With Property Contact Leads only reads what is already on the public property page, plus what a public Google search surfaces for the property's own name. Nothing behind a login, a partner account, or Booking.com's internal messaging system is touched.

### What data can I extract with Booking.com Scraper With Property Contact Leads?

Every row groups into three kinds of data: the contact lead itself, the pricing and stay details, and the full property record underneath it.

#### Contact and lead fields

| Field Name | Description |
| ----- | ----- |
| `hasContact` | `true` if any email, phone or website was found for the property |
| `contactEmail` | Property email — on-page trader email first, else SERP-backfilled, else `null` |
| `contactPhone` | Property phone — on-page trader phone first, else SERP-backfilled, else `null` |
| `contactWebsite` | The property's own website, discovered via SERP backfill (aggregators excluded), or `null` |
| `contactCompany` | Registered business/company legal name from the trader block, or `null` |
| `contactName` | Host name or trader first/last name, or `null` |
| `emailDomain` | Domain portion of `contactEmail`, or `null` |
| `contactCountry` | Trader country code, falling back to the property's own country, or `null` |
| `isBusinessListing` | `true` when Booking.com flags the property as an EU-regulated trader listing |
| `contactSource` | `onpage_trader`, `serp`, or `null` — where the contact was found |

#### Pricing and stay fields

| Field Name | Description |
| ----- | ----- |
| `price` | Lowest room price found for the searched stay, or `null` |
| `currency` | Currency code for `price` |
| `checkIn` | Formatted check-in time window as published by the property (e.g. "From 15:00") |
| `checkOut` | Formatted check-out time window as published by the property |
| `checkInDate` | The `checkIn` date you requested in input, echoed back, or `null` |
| `checkOutDate` | The `checkOut` date you requested in input, echoed back, or `null` |
| `breakfast` | `"Available"` when breakfast is offered, or `null` |
| `rooms` | Array of room objects: `id`, `name`, `description`, `size`, `occupancy`, `price` |
| `roomImages` | Array of `{ roomId, images }` — photos grouped per room |

#### Property detail fields

| Field Name | Description |
| ----- | ----- |
| `order` | Row position within the run |
| `name` | Property name |
| `type` | Accommodation type as published (e.g. `hotel`) |
| `url` | Canonical Booking.com property URL |
| `source_url` | The exact URL this row was scraped from |
| `startUrlOrQuery` | The destination search string or start URL that produced this row |
| `hotelId` | Booking.com's internal property ID |
| `hotelChain` | Hotel chain/brand name, or `null` for independent properties |
| `address` | Object: `full`, `country`, `city` |
| `location` | Object: `lat`, `lng` |
| `breadcrumbs` | Array of `{ name, url }` — Booking.com's own category breadcrumb trail |
| `image` | Primary property image URL |
| `images` | Array of property image URLs (up to 50) |
| `stars` | Official star rating, or `null` |
| `rating` | Guest review score, or `null` |
| `ratingLabel` | Booking.com's text label for the score (e.g. "Superb") |
| `reviews` | Total guest review count |
| `categoryReviews` | Array of `{ name, score }` — per-category review breakdown (cleanliness, location, etc.) |
| `description` | Property description text |
| `highlights` | Array of highlighted facility/feature labels |
| `finePrint` | Concatenated fine-print text from the property |
| `policies` | Array of `{ name, value }` house-rule and policy entries |
| `facilities` | Array of `{ name, id }` facility entries |
| `licenseInfo` | Property license/registration number, or `null` |
| `hostInfo` | Host display name (independent hosts), or `null` |
| `traderInfo` | Object: `isBusiness`, `email`, `phone`, `companyName`, `firstName`, `middleName`, `lastName`, `registrationNumber`, `tradeRegisterName`, `address` (`street`, `street2`, `postalCode`, `city`, `countryCode`, `state`) |
| `timeOfScrapeISO` | UTC ISO timestamp of when the row was scraped |

All 46 fields are always present in every row — when `includePropertyDetails` is off, the detail arrays (`rooms`, `roomImages`, `images`, `facilities`, `highlights`, `categoryReviews`, `policies`) come back as empty lists rather than being dropped from the row, so your schema never changes between runs.

### ⚠️ Contact coverage is partial — plan outreach lists around it

This is the constraint worth reading before you build a campaign on the output. Booking.com does not publish most property owners' direct contact details — the platform routes guest and partner communication through its own messaging system. `enrichContactsViaSearch` (on by default) tries to close that gap with a keyless Google search — `"<hotel name>" <city> official website contact email`, then a `site:<domain>` follow-up query on the discovered domain — to recover the property's own website and a public email or phone when the on-page trader block is absent.

This backfill is best-effort, not a guarantee: it depends on the property having a discoverable public website with a listed contact, and on Google returning that page for the query. There is no published hit-rate figure for how often it succeeds — the Actor does not fabricate one, and it does not fabricate a contact either. A property with nothing findable comes back with `hasContact: false` and `null` in every contact field, never a guessed email. Turn on `contactsOnly` if you want your export to contain only rows that actually carry a usable contact.

### Why not build this yourself?

Booking.com does not publish an open, self-serve search API for general use. Its Connectivity/Demand API access is scoped to registered accommodation partners and travel affiliates managing their own existing inventory and bookings — not ad hoc bulk search-and-contact workflows like sourcing a lead list across a destination. That leaves scraping the public site as the practical route, and the engineering behind it is not trivial.

Booking.com renders its property data through a layered set of sources rather than one predictable place: an inline Apollo GraphQL state blob, schema.org JSON-LD, `window.booking` environment variables, and DOM fallbacks for whatever the first three miss. A scraper that only reads one of these breaks the moment Booking.com stops populating it for a given property. This Actor reads all four and merges them, so a missing source degrades one field instead of failing the row.

The second cost is the contact layer itself. Recovering a property's own website and public email through a keyless search means running an independent SERP-fetching pipeline: proxy tier management (`GOOGLE_SERP` datacenter group, escalating to residential), a block detector that keys off the actual results container rather than loose keyword matching (so a normal page that merely contains the word "captcha" isn't discarded as blocked), and filtering out aggregator domains like Tripadvisor, Expedia and Booking.com's own result pages so the discovered "website" is actually the property's. Building and maintaining that pipeline is a separate project from the scraper itself.

### How to use Booking.com Scraper With Property Contact Leads

1. Open Booking.com Scraper With Property Contact Leads on Apify and click **Try for free**
2. Enter a **Destination** (`search`) — or paste one or more **Booking.com Start URLs** (`startUrls`) to skip search entirely
3. Set **Max properties per destination or URL** (`maxItems`) to the number of leads you want
4. Leave **Backfill missing contacts via web search** (`enrichContactsViaSearch`) on to maximize contact coverage, or turn it off for a faster, on-page-only run
5. Optionally turn on **Contacts only** and/or set an **email-domain allow-list** to narrow the export to usable leads
6. Click **Start**, then export the dataset as JSON, CSV, Excel, XML or an HTML table

#### How to scale to bulk lead generation

`maxItems` caps rows per destination or per start URL, not a single global count when you pass multiple `startUrls` — each URL you list gets its own budget. Point `startUrls` at several Booking.com search-results pages (one per city, region, or chain search) to build a multi-destination lead list in a single run; each URL is scraped and enriched in turn, and rows are pushed to the dataset live as each property finishes, so you can start working the list before the run completes.

### What can you do with Booking.com property contact leads?

- 🏨 **A hospitality sales rep** sourcing partnership leads searches a destination, enables `contactsOnly`, and works the resulting `contactEmail`/`contactPhone` list directly into outreach — no manual lookup per property.
- 🏢 **A lead-generation agency** running this for a hotel-chain client sets `emailDomainFilter` to the chain's own domains (e.g. `["hilton.com", "ihg.com"]`) to isolate corporate-owned properties from independents in the same destination.
- 📊 **A travel-tech market researcher** pulls `price`, `stars`, `rating` and `propertyType` across a destination to map pricing distribution and category density without touching contact data at all.
- 🤖 **An AI agent pipeline** calls the Actor as a tool, filters the returned rows on `hasContact`, and hands the qualified subset to a downstream outreach or CRM-sync step — all through the Apify API, no scraping code of its own.

### How does Booking.com Scraper With Property Contact Leads handle rate limits and blocking?

Two independent proxy ladders, one for page fetching and one for contact search.

**Property pages** are fetched with `impit` (Chrome-impersonating HTTP) first, falling back to a headless Playwright Chromium session when `impit` comes back blocked, empty, or under a captcha challenge. Both run behind a proxy tier ladder — Residential first, then Datacenter, then no proxy — retrying up to three times per tier before escalating. A response is treated as blocked when it's under 3,000 characters or contains a captcha/robot-check marker, so a soft block that returns a normal-looking but empty page is caught rather than parsed into an empty row.

**Contact search** runs its own ladder: the keyless `GOOGLE_SERP` proxy group first, escalating to Residential if that's blocked, for up to five escalations. Block detection is keyed on the presence of Google's actual results container (`div#search`, `div#rso`, or an anchor-wrapped `h3`) rather than scanning for stray words like "captcha" — a real results page that happens to mention captcha in a snippet is not discarded. A genuine "no results" page is also detected separately and is not treated as a block, so a property that truly has no public footprint doesn't burn retries.

### ⬇️ Input sample

Required per schema: none. Every parameter below has a default and can be left as-is for a first run.

| Parameter | Required | Type | Description | Example Value |
| ----- | ----- | ----- | ----- | ----- |
| `search` | No | string | City, region or landmark to search for accommodation. Ignored when `startUrls` is provided. Default `""`. | `"New York"` |
| `startUrls` | No | array | One or more Booking.com URLs — hotel detail pages or search-results pages. Overrides `search`. Default `[]`. | `["https://www.booking.com/hotel/us/grand-example.html"]` |
| `maxItems` | No | integer | Maximum properties to scrape (and turn into leads) per destination or per start URL. Minimum `1`, maximum `20000`. Default `10`. | `50` |
| `enrichContactsViaSearch` | No | boolean | When on, properties without an on-page email/phone are looked up with a keyless `"<hotel name>" <city>` then `site:<domain>` Google search. When off, leads use only on-page trader data. Default `true`. | `true` |
| `contactsOnly` | No | boolean | When on, properties with no email, phone or website found are dropped from the output entirely. Default `false`. | `false` |
| `emailDomainFilter` | No | array | Allow-list of email domains. Only properties whose `contactEmail` matches one of these are kept. Empty keeps all. Default `[]`. | `["hilton.com", "ihg.com"]` |
| `includePropertyDetails` | No | boolean | When on, each row also carries rooms, facilities, images, category review scores and policies. Default `true`. | `true` |
| `propertyType` | No | string | Filter by accommodation type. Enum: `none`, `hotels`, `apartments`, `hostels`, `guest houses`, `homestays`, `bed and breakfasts`, `holiday homes`, `boats`, `villas`, `motels`, `resorts`, `holiday parks`, `campsites`, `luxury tents`. Default `"none"`. | `"hotels"` |
| `sortBy` | No | string | How to sort search results. Enum: `distance_from_search`, `price`, `review_score_and_price`, `review_score`, `class`. Default `"distance_from_search"`. | `"review_score_and_price"` |
| `minimumRating` | No | string | Minimum guest rating (e.g. `7`, `8`, `9`). Leave empty for no filter. Default `""`. | `"8"` |
| `starsCountFilter` | No | string | Filter by star rating. Enum: `any`, `1`, `2`, `3`, `4`, `5`. Default `"any"`. | `"4"` |
| `currency` | No | string | Currency for displayed prices. Enum: `USD`, `EUR`, `GBP`, `CAD`, `AUD`, `CHF`, `JPY`, `CNY`, `INR`, `BRL`, `MXN`. Default `"USD"`. | `"EUR"` |
| `language` | No | string | Interface language for the search. Enum: `en-gb`, `en-us`, `de`, `fr`, `es`, `it`, `pt-br`, `nl`, `pl`, `ru`, `ja`, `zh`. Default `"en-gb"`. | `"en-us"` |
| `checkIn` | No | string | Absolute date (`YYYY-MM-DD`) or relative (e.g. `2 weeks`). Leave empty for flexible search. Default `""`. | `"2026-09-10"` |
| `checkOut` | No | string | Absolute date (`YYYY-MM-DD`) or relative (e.g. `1 week`). Leave empty for flexible search. Default `""`. | `"2026-09-13"` |
| `flexWindow` | No | string | Allow check-in/check-out dates to shift by this many days in either direction. `"0"` is exact dates only. Enum: `0`, `1`, `2`, `3`, `7`. Default `"0"`. | `"3"` |
| `rooms` | No | integer | Number of rooms for the stay. Minimum `1`, maximum `9`. Default `1`. | `1` |
| `adults` | No | integer | Number of adult guests. Minimum `1`, maximum `30`. Default `2`. | `2` |
| `children` | No | integer | Number of children. Minimum `0`, maximum `30`. Default `0`. | `0` |
| `minMaxPrice` | No | string | Min-max price filter as a string: `"MIN-MAX"` (e.g. `"50-200"`) or `"MIN+"` for a minimum only (e.g. `"100+"`). Default `"0-999999"`. | `"100-300"` |
| `proxyConfiguration` | No | object | Proxy settings for property-page fetches. Residential is on by default — Booking.com blocks direct datacenter traffic. Default `{"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]}`. | `{"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]}` |

#### Example input

```json
{
  "search": "Paris",
  "maxItems": 25,
  "enrichContactsViaSearch": true,
  "contactsOnly": true,
  "emailDomainFilter": [],
  "includePropertyDetails": false,
  "propertyType": "hotels",
  "minimumRating": "8",
  "starsCountFilter": "4",
  "currency": "EUR",
  "checkIn": "2026-09-10",
  "checkOut": "2026-09-13",
  "flexWindow": "3",
  "minMaxPrice": "100-300"
}
```

**Common pitfall:** `minMaxPrice` must be a string in exactly `"MIN-MAX"` or `"MIN+"` form — a bare number like `"150"` has no `-` or `+` for the parser to split on, so the exception is caught silently and the price filter is simply not applied to the search. Also remember `search` is ignored the moment `startUrls` is non-empty — the two are either/or, not additive.

### ⬆️ Output sample

Typed, normalized JSON with the same 46 keys on every row, whether or not a value was found — missing contacts and detail arrays come back as `null` or `[]`, never as a dropped key. Rows are pushed to the dataset live as each property finishes, so you can start working the data before the run ends. Export as JSON, CSV, Excel, XML or HTML table, or read the dataset through the Apify API.

#### Example output

```json
{
  "order": 1,
  "url": "https://www.booking.com/hotel/fr/grand-example-paris.html",
  "startUrlOrQuery": "Paris",
  "name": "Grand Hotel Example Paris",
  "type": "hotel",
  "description": "A boutique hotel near the city centre with 42 rooms.",
  "stars": 4,
  "price": 289,
  "currency": "EUR",
  "rating": 8.6,
  "ratingLabel": "Very good",
  "reviews": 1204,
  "breakfast": "Available",
  "checkIn": "From 15:00",
  "checkOut": "Until 11:00",
  "checkInDate": "2026-09-10",
  "checkOutDate": "2026-09-13",
  "location": { "lat": 48.8566, "lng": 2.3522 },
  "address": { "full": "12 Rue de Example", "country": "FR", "city": "Paris" },
  "image": "https://cf.bstatic.com/xdata/images/hotel/max1024x768/example1.jpg",
  "rooms": [
    { "id": "101", "name": "Deluxe Double Room", "description": "City view, 24 sqm", "size": "24 m²", "occupancy": 2, "price": 289 }
  ],
  "highlights": ["Free WiFi", "Non-smoking rooms"],
  "finePrint": "Guests are required to show a photo ID and credit card at check-in.",
  "policies": [
    { "name": "Check-in", "value": "From 15:00" },
    { "name": "Pets", "value": "Pets are not allowed." }
  ],
  "images": [
    "https://cf.bstatic.com/xdata/images/hotel/max1024x768/example1.jpg",
    "https://cf.bstatic.com/xdata/images/hotel/max1024x768/example2.jpg"
  ],
  "roomImages": [
    { "roomId": "101", "images": ["https://cf.bstatic.com/xdata/images/hotel/max1024x768/room101.jpg"] }
  ],
  "categoryReviews": [
    { "name": "Cleanliness", "score": 8.9 },
    { "name": "Location", "score": 9.4 }
  ],
  "hotelChain": null,
  "licenseInfo": "FR-PARIS-000123",
  "hostInfo": null,
  "traderInfo": {
    "isBusiness": true,
    "email": "reservations@grandhotelexampleparis.com",
    "phone": "+33123456789",
    "companyName": "Grand Hotel Example Paris SARL",
    "firstName": null,
    "middleName": null,
    "lastName": null,
    "registrationNumber": "RCS Paris 123 456 789",
    "tradeRegisterName": "RCS Paris",
    "address": {
      "street": "12 Rue de Example",
      "street2": null,
      "postalCode": "75001",
      "city": "Paris",
      "countryCode": "FR",
      "state": null
    }
  },
  "breadcrumbs": [
    { "name": "France", "url": "https://www.booking.com/country/fr.html" },
    { "name": "Paris", "url": "https://www.booking.com/city/fr/paris.html" }
  ],
  "hotelId": "1234567",
  "facilities": [
    { "name": "Free WiFi", "id": "1" },
    { "name": "Airport shuttle", "id": "2" }
  ],
  "timeOfScrapeISO": "2026-07-25T10:32:11.402931+00:00",
  "source_url": "https://www.booking.com/hotel/fr/grand-example-paris.html",
  "hasContact": true,
  "contactEmail": "reservations@grandhotelexampleparis.com",
  "contactPhone": "+33123456789",
  "contactWebsite": "https://grandhotelexampleparis.com/",
  "contactCompany": "Grand Hotel Example Paris SARL",
  "contactName": null,
  "emailDomain": "grandhotelexampleparis.com",
  "contactCountry": "FR",
  "isBusinessListing": true,
  "contactSource": "onpage_trader"
}
```

### How do you filter and target specific properties?

Targeting starts with how you point the Actor at Booking.com: **destination search vs. direct URLs**. `search` is more accurate for casual discovery — Booking.com applies its own relevance ranking to a city or region name — while `startUrls` is more precise when you already have a Booking.com search-results URL with filters (dates, price, star rating) baked into the query string, or a specific set of hotel detail pages you already know you want.

**Scope precision** comes from `propertyType`, `starsCountFilter`, `checkIn`/`checkOut` with `flexWindow`, `rooms`/`adults`/`children`, and `minMaxPrice` — narrow to hotels only, four-star and up, a specific stay window with a few days of flexibility, and a price band, all before a single detail page is scraped.

**Quality and contact thresholds** are `minimumRating` for guest rating, and — unique to this Actor — `contactsOnly` and `emailDomainFilter` for the lead itself: keep only rows with a usable contact, or only rows whose email matches a chain's own domain. **Volume** is controlled with `maxItems`, applied per destination or per start URL.

```json
{ "search": "Barcelona", "propertyType": "hotels", "starsCountFilter": "4", "minMaxPrice": "80-250" }
```

```json
{ "startUrls": ["https://www.booking.com/searchresults.html?ss=Rome&checkin=2026-10-01&checkout=2026-10-04"], "flexWindow": "3", "adults": 2 }
```

```json
{ "search": "Miami", "minimumRating": "8", "contactsOnly": true, "emailDomainFilter": ["marriott.com", "hilton.com"] }
```

Note that `contactsOnly` and `emailDomainFilter` stack with **AND** logic when both are set: a row must have a `contactEmail` matching one of the listed domains, and (redundantly, since a matching email already implies `hasContact: true`) also satisfy `contactsOnly`. In practice, setting `emailDomainFilter` alone already limits you to rows with a matching contact — `contactsOnly` only adds something new when `emailDomainFilter` is empty.

### How does it work?

Property pages are parsed in layers, richest source first: an inline Apollo GraphQL state blob (`data-capla-store-data="apollo"`), schema.org JSON-LD, `window.booking` environment variables, then DOM selectors as a last resort. Each layer is best-effort and results are merged, so a missing or restructured source degrades individual fields rather than failing the row.

Contacts are built the same way, layered rather than single-sourced. The on-page trader/business block — populated for EU-regulated and registered-business listings — is read first. When it's empty, `enrichContactsViaSearch` runs a two-step keyless Google search: first `"<hotel name>" <city> official website contact email` to locate the property's own domain (filtering out aggregators like Booking.com, Tripadvisor, Expedia and Airbnb), then a `site:<domain>` follow-up query mining that domain specifically for a public email or phone. `null` is returned over a fabricated value at every step.

### Integrations

Booking.com Scraper With Property Contact Leads is an Apify Actor, so it works with anything that can call the Apify API or consume a dataset.

#### Python example

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")

run = client.actor("<YOUR_USERNAME>/booking-scraper-with-property-contact-leads").call(run_input={
    "search": "Lisbon",
    "maxItems": 30,
    "contactsOnly": True,
    "emailDomainFilter": [],
})

for lead in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(lead["name"], lead["contactEmail"], lead["contactPhone"], lead["price"])
```

Works in Go, Ruby, Node.js, cURL — any language that can make an HTTP request.

#### Export to spreadsheets or CRM

Export the dataset as CSV or Excel directly from the Apify Console and import it into a CRM or spreadsheet — map `name`, `contactEmail`, `contactPhone`, `contactWebsite`, `contactCompany` and `contactCountry` to your lead columns, keeping `hasContact` and `contactSource` alongside them so you can see coverage and provenance at a glance.

### Is it legal to scrape Booking.com?

Scraping publicly accessible listing data is broadly treated as permissible where no login or authentication is bypassed, and this Actor reads only what Booking.com already shows an anonymous visitor plus what a public Google search surfaces.

Property listings, pricing, ratings and the trader/business contact block are business records, not personal data about a private individual — so the legal lens that applies here is Booking.com's terms of service and database/unfair-competition rules around systematic reuse of a compiled listing set, not GDPR-style personal-data regimes.

One responsibility note, not legal advice: if you use the contact fields as an outreach lead list, complying with applicable messaging and email laws in your jurisdiction — such as CAN-SPAM in the US, or PECR and GDPR's rules on unsolicited B2B email in the EU/UK — is on you as the sender. Consult legal counsel for commercial outreach campaigns built on this data.

### ❓ Frequently asked questions

#### What if a property has no email or phone found after enrichment?

It's returned with `hasContact: false` and `null` in `contactEmail`, `contactPhone` and `contactWebsite` — never a guessed value. Booking.com hides most direct property contacts, and the keyless SERP backfill is best-effort, so some properties genuinely have nothing publicly findable. Turn on `contactsOnly` to drop these rows from your export entirely.

#### Can I get the full property details along with the contact leads?

Yes. `includePropertyDetails` (default `true`) adds rooms, facilities, images, category review scores and policies to every row. Turn it off for a lean, contact-focused record — the row keys stay the same either way, just with empty arrays where the detail would be.

#### How accurate is the contact and price data?

The Actor returns data exactly as it appears on the public listing (or public search result, for SERP-backfilled contacts) at request time. Accuracy depends on whether the property keeps its own listing and website current. For high-volume outreach, spot-check a sample of `contactEmail`/`contactPhone` rows before a full send.

#### How many properties can I get per run?

`maxItems` accepts 1 to 20,000, applied per destination or per start URL — pass multiple `startUrls` to multiply that budget across several searches in one run.

#### How do `contactsOnly` and `emailDomainFilter` interact?

They combine with AND logic. `emailDomainFilter` keeps only rows whose `contactEmail` matches an allowed domain; `contactsOnly` separately drops any row with `hasContact: false`. Since a matching email already implies `hasContact: true`, setting both together behaves the same as `emailDomainFilter` alone.

#### What's the difference between `search` and `startUrls`?

`search` runs a destination search on Booking.com. `startUrls` takes direct Booking.com URLs — hotel pages or search-results pages — and is used instead of `search` whenever it's non-empty; the two are not combined.

#### Is a Booking.com account, login or API key required?

No. Property scraping and the contact-search backfill are both keyless — no Booking.com login, no Google API key, no third-party enrichment key. The only credential you need is your Apify token.

#### Does Booking.com Scraper With Property Contact Leads work with Claude, ChatGPT and AI agent frameworks?

Yes. It's callable as a standard HTTP endpoint through the Apify API, so LangChain, CrewAI, n8n or a hand-written tool definition can invoke it and receive typed JSON with no parsing step.

#### How does this compare to other Booking.com and contact-lead scrapers?

Checked on the Apify Store on 25 July 2026: `meticulous_snail/booking-hotel-owner-leads` targets the same niche — Booking.com hotel and property owner leads, listed free — but its Store page currently has no published README, so its extraction method and field set are not documented. Outside the Booking.com niche, `alizarin_refrigerator-owner/company-contact-enricher` enriches arbitrary company websites into B2B contacts but requires you to bring your own Apollo.io, Hunter.io, Anthropic and Firecrawl API keys for live data (a free demo mode is available). `bovi/zoominfo-scraper` reads public ZoomInfo company pages with a fully managed proxy and no user-supplied key, but returns ZoomInfo's own masked current emails/phones plus historical addresses, not a property's direct contact. This Actor's difference is that it sources leads directly from Booking.com property pages with no external API key of any kind — the SERP backfill is keyless.

#### Can I filter properties by price, stay dates or type before I scrape?

Yes. `propertyType`, `starsCountFilter`, `minimumRating`, `minMaxPrice`, and `checkIn`/`checkOut` with `flexWindow` are all applied on the Booking.com search itself, so you narrow the result set before any detail page — and any contact enrichment — happens.

### 🔗 Related scrapers

| Scraper Name | What it extracts |
| ----- | ----- |
| Apartments.com Scraper — Floor Plan & Units | Apartment listings with floor plans and unit-level pricing and availability |
| Realtor.com Agent Scraper — Recent Sold Listings | Real estate agent profiles paired with their recent sold-listing history |
| Google Maps Scraper — By Radius / GeoJSON Territory | Local business listings within a custom radius or territory boundary |
| LinkedIn Company URL Mass Profile Finder — Executive Contacts | Company profiles enriched with executive contact leads |
| Instagram Phone and Email Lead Finder | Instagram profiles enriched with phone and email contact leads |

### Conclusion

Booking.com Scraper With Property Contact Leads turns a destination search or a list of Booking.com URLs into a property dataset with a contact lead attached to every row — on-page trader data first, best-effort keyless search backfill second, and `null` instead of a guess whenever neither turns up anything. The output stays the same 46-key shape whether you're pulling a lean lead list with `contactsOnly` or the full property record with rooms and facilities. Start it from the Apify Console or call it through the Apify API.

### 💬 Your feedback

Found a bug, or need a contact or property field that Booking.com publishes but this Actor doesn't return yet? Open an issue on the Actor's Issues tab — reports that include the property URL are the fastest to reproduce and fix.

# Actor input Schema

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

City, region or landmark to search for accommodation (e.g. New York, Paris). Ignored when Start URLs are provided.

## `startUrls` (type: `array`):

One or more Booking.com URLs — hotel detail pages or search-results pages. Overrides the destination above.

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

Maximum number of properties to scrape (and turn into leads) per destination or per start URL.

## `enrichContactsViaSearch` (type: `boolean`):

When on, properties without an on-page email/phone are looked up with a keyless `"<hotel name>" <city>` then `site:<domain>` Google search to recover a website + public contact. When off, leads use only on-page trader data (faster, lower coverage). Default is true.

## `contactsOnly` (type: `boolean`):

When on, properties for which no email, phone or website could be found are dropped from the output entirely — you only keep rows that carry a usable contact. Default is false (keep every property).

## `emailDomainFilter` (type: `array`):

Optional allow-list of email domains (e.g. hilton.com, marriott.com). When set, only properties whose contactEmail matches one of these domains are kept. Leave empty to keep all. Example: \["hilton.com", "ihg.com"].

## `includePropertyDetails` (type: `boolean`):

When on, each row also carries rooms, facilities, images, category review scores and policies. Turn off for a lean, contact-focused lead record. Default is true.

## `propertyType` (type: `string`):

Filter by type of accommodation.

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

How to sort search results.

## `minimumRating` (type: `string`):

Minimum guest rating (e.g. 7, 8, 9). Leave empty for no filter.

## `starsCountFilter` (type: `string`):

Filter by star rating.

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

Currency for displayed prices.

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

Interface language for the search.

## `checkIn` (type: `string`):

Absolute date (YYYY-MM-DD) or relative (e.g. 2 weeks). Leave empty for flexible search.

## `checkOut` (type: `string`):

Absolute date (YYYY-MM-DD) or relative (e.g. 1 week). Leave empty for flexible search.

## `flexWindow` (type: `string`):

Allow dates to shift by this many days. Use 0 for exact dates only.

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

Number of rooms for the stay.

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

Number of adult guests.

## `children` (type: `integer`):

Number of children.

## `minMaxPrice` (type: `string`):

Min-max price filter, e.g. 50-200 or 100+ for minimum only.

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

Residential proxy is enabled by default for Booking.com page fetches. You can override here.

## Actor input object example

```json
{
  "search": "New York",
  "startUrls": [],
  "maxItems": 10,
  "enrichContactsViaSearch": true,
  "contactsOnly": false,
  "emailDomainFilter": [],
  "includePropertyDetails": true,
  "propertyType": "none",
  "sortBy": "distance_from_search",
  "minimumRating": "",
  "starsCountFilter": "any",
  "currency": "USD",
  "language": "en-gb",
  "checkIn": "",
  "checkOut": "",
  "flexWindow": "0",
  "rooms": 1,
  "adults": 2,
  "children": 0,
  "minMaxPrice": "0-999999",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `results` (type: `string`):

All scraped items in the Actor's default dataset.

# 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 = {
    "search": "New York",
    "startUrls": [],
    "maxItems": 10,
    "enrichContactsViaSearch": true,
    "contactsOnly": false,
    "emailDomainFilter": [],
    "includePropertyDetails": true,
    "propertyType": "none",
    "sortBy": "distance_from_search",
    "minimumRating": "",
    "starsCountFilter": "any",
    "currency": "USD",
    "language": "en-gb",
    "checkIn": "",
    "checkOut": "",
    "flexWindow": "0",
    "rooms": 1,
    "adults": 2,
    "children": 0,
    "minMaxPrice": "0-999999",
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("simpleapi/booking-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 = {
    "search": "New York",
    "startUrls": [],
    "maxItems": 10,
    "enrichContactsViaSearch": True,
    "contactsOnly": False,
    "emailDomainFilter": [],
    "includePropertyDetails": True,
    "propertyType": "none",
    "sortBy": "distance_from_search",
    "minimumRating": "",
    "starsCountFilter": "any",
    "currency": "USD",
    "language": "en-gb",
    "checkIn": "",
    "checkOut": "",
    "flexWindow": "0",
    "rooms": 1,
    "adults": 2,
    "children": 0,
    "minMaxPrice": "0-999999",
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("simpleapi/booking-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 '{
  "search": "New York",
  "startUrls": [],
  "maxItems": 10,
  "enrichContactsViaSearch": true,
  "contactsOnly": false,
  "emailDomainFilter": [],
  "includePropertyDetails": true,
  "propertyType": "none",
  "sortBy": "distance_from_search",
  "minimumRating": "",
  "starsCountFilter": "any",
  "currency": "USD",
  "language": "en-gb",
  "checkIn": "",
  "checkOut": "",
  "flexWindow": "0",
  "rooms": 1,
  "adults": 2,
  "children": 0,
  "minMaxPrice": "0-999999",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call simpleapi/booking-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,simpleapi/booking-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/jr2qLCJruhHfPP22V/builds/r1bGfEgZC6z7Sa7of/openapi.json
