# Airbnb Scraper & Market Monitor (`scrapesage/airbnb-scraper`) Actor

Reliable Airbnb scraper: location/URL search, property details, availability calendar (with prices), and reviews in one Actor — plus an incremental monitoring mode that tracks new listings, price changes, availability changes and delistings over time. Public listing & host-operational data only.

- **URL**: https://apify.com/scrapesage/airbnb-scraper.md
- **Developed by:** [Scrape Sage](https://apify.com/scrapesage) (community)
- **Categories:** Travel, Real estate, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.00 / 1,000 listing scrapeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## What's an Apify Actor?

Actors are a software tools running on the Apify platform, for all kinds of web data extraction and automation use cases.
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.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp.md).

If your project is in a different language, use 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

## Airbnb Scraper & Market Monitor

Scrape Airbnb at scale and **track a market over time** — in one Actor. Search by location
or paste Airbnb search/room URLs, then get full property details, the availability calendar,
host operational data, and reviews. Re-run on a schedule in
**incremental monitoring mode** to capture new listings, price changes, availability changes
and delistings — so you watch a market, not just snapshot it.

> ℹ️ **Per‑day calendar nightly prices are temporarily unavailable** (returning ~**July 10, 2026**
> while billing is finalized). The calendar currently returns day‑by‑day availability; enabling
> `calendarPrices` has no effect until then.

Built for revenue managers, short‑term‑rental investors, market analysts, and developers who
need reliable, structured Airbnb data.

> **Public data only.** This Actor captures public listing, pricing, availability and
> host *operational* data. It intentionally **excludes personal identifiers** of hosts,
> co‑hosts and reviewers (name, profile photo, profile URL, "about" text). See
> [Data scope](#data-scope--privacy).

---

### Why this Actor

| | This Actor | Typical Airbnb scraper |
|---|---|---|
| Search + property detail + calendar + reviews | ✅ all in one Actor | often split across separate actors |
| Beyond the ~270-result search cap | ✅ automatic price‑band + map splitting | partial or manual |
| **Incremental monitoring** (new / price / availability / delisting) | ✅ **built in** | rarely offered |

**Reliability:** an API‑first tiered fetch — Airbnb's own GraphQL/JSON endpoints first,
with a Playwright browser fallback — runs over **datacenter proxy by default with automatic
residential fallback**, plus session rotation, rate limiting, exponential backoff and
circuit breakers. In testing, a **1,000‑listing run with full detail and reviews completed
with 0 blocks and every listing returned**.

The differentiators are **all‑in‑one coverage, incremental monitoring and reliability**,
backed by a rich field set: availability calendar, registration number where shown, host
operational signals, grouped amenities and house‑rules, and the full photo gallery.

---

### Features

- **Multiple sources in one run:** location queries, pre-filtered search URLs, and
  direct listing/room URLs.
- **Tiered, instrumented fetch** for reliability — *Airbnb's GraphQL/JSON endpoints first*,
  *Playwright browser fallback* if needed — routed through **datacenter proxy by default
  with automatic residential fallback**, plus rotating sessions and exponential backoff.
  The run summary reports which tier served results so you can *measure* reliability.
- **Beyond the ~270-result cap:** automatic **price‑band splitting** (primary) and
  optional **map bounding‑box subdivision** (dense cities), with dedupe by listing id.
- **Availability calendar** — day‑by‑day available / min‑/max‑nights / check‑in‑out
  eligibility. *(Optional per‑day nightly price (`calendarPrices`) is temporarily
  unavailable — returning ~July 10, 2026; see the note at the top.)*
- **Reviews** (content, rating, date, language, opaque reviewer id only).
- **Incremental monitoring mode:** baseline once, then emit only what changed.
- **Billing fairness:** empty/unavailable/delisted/zero‑result/region‑blocked outcomes
  are **successful runs that explain themselves and are not charged**.
- **Layout/endpoint‑rot detector:** loud warning if Airbnb's markup/GraphQL changes.

---

### Input

Provide at least one source. All fields are documented in the Actor's input form.

**Defaults that matter:** the actor returns the **richest** data out of the box — detail
pages are **ON** by default (`skipDetailPages: false`) and **reviews are ON**
(`scrapeReviews: true`), so each listing comes back with description, amenities,
accessibility, house rules, registration, full gallery, host data (incl. time as host and
response rate/time), and reviews. For a fast, cheap run, set `skipDetailPages: true` to get
search-card data only (title, price, rating, coordinates, badges, Superhost/Guest-favorite,
thumbnail), and/or `scrapeReviews: false`. `maxListings` defaults to **100**; raise it for
bigger pulls or set `0` for unlimited (a dense city can be many thousands). A listing URL
passed directly is always fetched in full regardless of `skipDetailPages`.

| Group | Fields |
|---|---|
| **Sources** | `locationQueries`, `searchUrls`, `listingUrls` |
| **Search filters** | `checkIn`, `checkOut`, `adults`, `children`, `infants`, `pets`, `priceMin`, `priceMax`, `minBeds`, `minBedrooms`, `minBathrooms`, `propertyTypes`, `currency`, `locale` |
| **Detail / calendar / reviews** | `skipDetailPages`, `calendarMonths` (0–12), `calendarPrices` *(per‑day nightly price — temporarily unavailable until ~July 10, 2026)*, `maxCalendarPriceDays`, `scrapeReviews`, `maxReviewsPerListing` |
| **Scale & cap-splitting** | `maxListings`, `priceSplitCeiling`, `enableMapSplit` |
| **Incremental monitoring** | `incrementalMode`, `monitorKey`, `trackPriceChanges`, `trackAvailabilityChanges`, `detectDelistings` |
| **Proxy & tuning** | `proxyConfiguration` (default datacenter, auto residential fallback), `maxConcurrency`, `maxRequestsPerMinute`, `maxRequestRetries` |

`propertyTypes` accepts Airbnb room types: `Entire home/apt`, `Private room`,
`Shared room`, `Hotel room` (unknown values are ignored).

#### Example: location search with calendar

```json
{
  "locationQueries": ["Lisbon, Portugal"],
  "checkIn": "2026-07-01",
  "checkOut": "2026-07-05",
  "adults": 2,
  "currency": "EUR",
  "calendarMonths": 2,
  "maxListings": 200,
  "proxyConfiguration": { "useApifyProxy": true }
}
````

#### Example: pre-filtered search URL

```json
{
  "searchUrls": ["https://www.airbnb.com/s/Miami-Beach--FL/homes?amenities%5B%5D=4"],
  "skipDetailPages": true
}
```

#### Example: single property + calendar + reviews

```json
{
  "listingUrls": ["https://www.airbnb.com/rooms/12345678"],
  "calendarMonths": 6,
  "scrapeReviews": true,
  "maxReviewsPerListing": 100,
  "currency": "USD"
}
```

> **Per‑day nightly prices — temporarily unavailable (returning ~July 10, 2026).** Airbnb's
> availability calendar contains no prices, so a nightly rate only exists as a dated booking
> quote (one request per day). We're rolling this out as an optional add‑on (`calendarPrices`)
> billed per priced day; until it goes live (~July 10, 2026) the calendar returns availability
> only and enabling `calendarPrices` has no effect. This note will be updated when it ships.

#### Example: incremental monitoring (re-run on a schedule)

```json
{
  "locationQueries": ["Lisbon, Portugal"],
  "checkIn": "2026-07-01",
  "checkOut": "2026-07-05",
  "calendarMonths": 1,
  "incrementalMode": true,
  "monitorKey": "lisbon-2br-summer",
  "trackPriceChanges": true,
  "trackAvailabilityChanges": true,
  "detectDelistings": true
}
```

***

### Output

Every record shares a common envelope and then carries the listing superset. Example
(abridged):

```json
{
  "id": "1001",
  "url": "https://www.airbnb.com/rooms/1001",
  "scrapedAt": "2026-06-06T09:00:00.000Z",
  "sourceType": "locationSearch",
  "sourceQuery": "Lisbon, Portugal",
  "status": "ok",
  "fetchTier": "ssr-json",
  "title": "Sunny Studio in Alfama",
  "description": "Charming studio with river views...",
  "propertyType": "Entire rental unit",
  "roomType": "Entire home/apt",
  "homeTier": 1,
  "personCapacity": 4,
  "minNights": 2,
  "registrationNumber": "Exempt - AL/12345",
  "coordinates": { "lat": 38.7139, "lng": -9.1334 },
  "images": [{ "url": "https://.../1001-1.jpg", "caption": "Living room", "orientation": "LANDSCAPE" }],
  "amenities": [{ "title": "Bathroom", "values": [{ "title": "Hair dryer", "available": true }] }],
  "accessibilityFeatures": [{ "title": "Guest entrance and parking", "values": [{ "title": "Step-free guest entrance", "available": true }, { "title": "Wide entrance", "available": false }] }],
  "pricing": {
    "price": { "value": 95, "raw": "€95", "currency": "EUR" },
    "originalPrice": { "value": 120, "raw": "€120", "currency": "EUR" },
    "priceLabel": "Was €120, now €95 per night",
    "qualifier": "night"
  },
  "rating": { "guestSatisfaction": 4.92, "cleanliness": 4.95, "reviewsCount": 218 },
  "isGuestFavorite": true,
  "badges": ["Superhost", "Guest favorite"],
  "host": { "hostId": "host-77", "isSuperhost": true, "isVerified": true, "ratingAverage": 4.9, "ratingCount": 540, "timeAsHost": { "years": 6, "months": 4 }, "responseRate": 100, "responseTime": "within an hour" },
  "calendar": [{ "date": "2026-07-01", "available": true, "minNights": 2, "maxNights": 30, "checkInEligible": true, "checkOutEligible": true }],
  "reviews": [{ "reviewId": "rev-1", "text": "Amazing stay!", "rating": 5, "createdAtISO": "2026-05-01T10:00:00.000Z", "language": "en", "reviewerId": "user-501" }]
}
```

> `calendar[]` currently carries availability fields only. A per‑day `price` field will be
> added for available, check‑in‑eligible days when the optional `calendarPrices` add‑on ships
> (~July 10, 2026).

#### Status field

| `status` | Meaning | Charged? |
|---|---|---|
| `ok` | A real listing result | ✅ |
| `empty:unavailable` | Listing exists but not bookable for the requested dates | ❌ |
| `empty:delisted` | Property removed/404 | ❌ |
| `empty:region_blocked` | Region/consent wall | ❌ |
| `empty:zero_results` | Search genuinely returned nothing | ❌ |
| `error:parserStale` | Result items **were** found but none parsed — Airbnb changed its result shape and the parser needs updating. Reported as a flagged fault (not `empty:zero_results`) so a populated market is never silently shown as empty. | ❌ |

A `runSummary` record (with `recordType: "runSummary"`) is emitted at the end and is
never charged. If any search hit `error:parserStale`, the summary carries a non‑zero
`parserStale` count and a LOUD rot warning is logged.

***

### Incremental monitoring mode

This is a recurring-revenue feature few other Airbnb scrapers offer — built for revenue
managers, STR investors and market analysts.

1. Turn on `incrementalMode` and set a stable `monitorKey` (e.g. `lisbon-2br-summer`).
   State is persisted in a **named Key‑Value store** (`airbnb-monitor-<monitorKey>`)
   across runs.
2. **First run** captures a baseline; every record is marked `changeType: "baseline"`.
3. **Later runs** compare each listing to its snapshot and emit only change records:
   - `new` — a listing id not seen before,
   - `priceChanged` — with `changeDetails.price = { old, new, delta, currency }`,
   - `availabilityChanged` — opened/closed (and calendar `availableDays` deltas),
   - `ratingChanged` — guest‑satisfaction or review‑count moved,
   - `delisted` — previously seen, now absent from the market (emitted after the full
     scan).
4. **Cost:** only `ok` listing/change records are charged. A monitoring run that finds
   **no changes costs about the actor‑start only** — cheap and sticky. Schedule it daily
   or hourly via Apify Schedules.

***

### Reliability & the run summary

Airbnb actively blocks scrapers, so reliability is engineered in depth. This Actor's
defense:

- **Datacenter proxy by default with automatic residential fallback** and rotating
  sessions; rotate on any 403/429/captcha. (Recent 1,000‑listing detail+reviews test runs
  held on datacenter with 0 blocks; residential takes over only if datacenter starts failing.)
- Realistic, consistent per‑session headers (UA, locale, currency, the public API key
  extracted at runtime); jittered delays; conservative concurrency.
- Exponential backoff with jitter; capped retries, then **classify and move on** — one
  bad listing never fails the run.

The end‑of‑run `runSummary` reports searches, listings found/after‑dedup/emitted,
calendar days, reviews, **fetch‑tier mix (SSR‑JSON vs GraphQL vs Playwright)**, blocks,
retries, session rotations, splits used and whether any search hit the ~270 cap. If
api‑key/hash extraction repeatedly fails or many pages parse to zero fields, a **loud
"possible Airbnb change" warning** is logged.

***

### Data scope & privacy

**Included:** public listing, pricing, availability, ratings; host **operational**
signals (opaque host id, isSuperhost, isVerified, host rating average/count, time as
host, **host response rate, host response time**, property registration/license number).

**Excluded by design — never in output:** host/co‑host/reviewer **name, profile photo,
profile URL, "about" text**, and host **personal attributes** that Airbnb shows on the
profile card (age/"born in" decade, occupation, school, hometown, languages, personal
bio highlights) — these identify an individual and are not collected. Host **listings
count** is also excluded: it lives only in a separate host‑profile request and is not
worth the extra cost or the broader personal‑profile footprint. Reviews keep
text/rating/date/language and an **opaque reviewer id only**. No emails, phone numbers
or contact data (Airbnb does not expose these pre‑booking). A guard scrubs output and an
automated test fails the build if any excluded field ever appears.

***

### FAQ

**Why do some listings come back as `empty:unavailable` instead of failing?**
Because they are valid public pages that simply aren't bookable for your dates. That's a
successful, explained, **un‑charged** result — not an error.

**Why might calendar/review fetches fall back to the browser tier?**
Calendar and reviews use Airbnb's persisted‑query GraphQL operations, whose hashes
rotate. When a hash can't be resolved from the page, the Actor uses the Playwright
fallback automatically.

**Does it exceed Airbnb's ~270 results per search?**
Yes — it splits the search by price band (and optionally by map area for dense cities)
and dedupes by listing id. The summary reports cap hits and split counts.

**Is the Airbnb API key hardcoded?** No. The `X-Airbnb-Api-Key` is Airbnb's own *public*
web client key, extracted from the page at runtime and refreshed if it rotates.

***

### Pricing (pay‑per‑event)

Charged events: `actor-start` (once per run) and `listing` (per `ok` listing/change
record). Empty/delisted/summary records are not charged. Dollar amounts and volume
tiers are configured by the publisher in the Apify Console.

### Development

```bash
npm install        # install deps
npm test           # offline parser + incremental tests (no network)
npm run lint       # node --check across all source files
npm run smoke      # ONLINE smoke test (needs network + Apify proxy)
```

See [`VERIFICATION.md`](./VERIFICATION.md) for exactly what has been verified vs what
requires a live Apify run.

### Changelog

#### 0.1.21

- **Added the Actor output schema (`output_schema.json`) and a full dataset field schema.**
  `.actor/output_schema.json` (linked via `output` in `actor.json`) declares that results live in
  the default dataset, so the Store/Console **Output** tab renders properly. `dataset_schema.json`
  now also documents every output field — types, titles, descriptions — so the field-level data
  contract is visible in the Store and API. The dataset schema is permissive
  (`additionalProperties`, nothing required) and was validated against a full 1,000-listing run plus
  the run-summary record with zero rejections, so it never blocks output.
- **README accuracy pass.** Removed unsourced competitor success-rate figures, corrected the proxy
  description to datacenter-first with automatic residential fallback, and stated observed
  reliability from real runs (a 1,000-listing detail+reviews run completed with 0 blocks, every
  listing returned). Softened absolute competitor claims to defensible, hedged statements.

#### 0.1.20

- **Richest data by default.** New runs now scrape full per-listing detail (`skipDetailPages`
  defaults OFF) **and** reviews (`scrapeReviews` defaults ON) out of the box, so a first run shows
  the actor's complete output without tuning. A fast/cheap search-only mode is still one toggle
  away. (Cost note: detail + reviews on is ~2× the requests of search-only — priced accordingly.)
- **No more misleading 0-star ratings.** A brand-new host or listing with no reviews previously
  surfaced `ratingAverage`/rating scores as `0`, which read like a 0-star score. Rating *averages*
  of 0 are now omitted (blank = "no rating yet"); review *counts* of 0 are unaffected.

#### 0.1.19

- **Richer host data (no extra requests).** Added **time as host** (years/months), **host
  response rate**, and **host response time** — all pulled from the host section already in the
  detail call, so zero added cost. New columns `Host response rate` / `Host response time` in the
  Overview. Host personal‑profile attributes (age, occupation, school, hometown, languages, bio
  highlights) are deliberately NOT collected — they identify individuals and fall outside the
  public‑professional‑data line. Host listings‑count is also excluded (host‑profile‑only; not
  worth the extra request).

#### 0.1.18

- **Lower cost + faster detailed scraping (POST-only detail path).** Detailed listings now
  fetch via a single lightweight `StaysPdpSections` API call instead of downloading the full
  listing HTML page — the biggest item on the wire. Fewer requests per listing and much less
  bandwidth, with no loss of fields: the host section is fetched in the same call, and if a
  response ever comes back incomplete the actor automatically falls back to the full-page
  method, so coverage never silently regresses.
- **Cost-efficient datacenter-first proxy with automatic residential fallback.** The default
  proxy is now datacenter (far cheaper per GB); if Airbnb blocks a datacenter request the
  actor escalates that request to residential automatically, and switches the whole run to
  residential if datacenter proves unreliable. You get datacenter cost with residential
  reliability as a safety net. Force residential anytime by selecting the RESIDENTIAL group.
  The run log reports the datacenter/residential split so the cost effect is measurable.

#### 0.1.17

- **Accessibility coverage fix.** The accessibility section ships an empty `seeAll…` stub
  alongside a populated `preview…` array on most listings; the parser was stopping at the
  empty stub and returning nothing (only ~1–2% of listings populated). It now picks the
  richest non-empty group array across both the default and modal accessibility sections,
  so accessibility features populate whenever the listing has them (and still return the
  full `seeAll` list when that one is the populated one). Confirmed against the live section
  shape captured by the 0.1.16 diagnostics.

#### 0.1.16

- Internal diagnostics only (no behavior or schema change): the run-end section log now
  also reports the accessibility section's nested group/item key shape and the metadata
  carried by the `StaysPdpSections` response. These confirm (a) whether low accessibility
  coverage is genuine host sparsity vs. a parser key mismatch, and (b) whether the detail
  path can drop its separate SSR page fetch in favor of the POST alone (a planned speedup).

#### 0.1.15

- **Accessibility features captured (new `accessibilityFeatures` field).** Detail runs now
  extract Airbnb's accessibility section — step-free entrances, accessible parking, grab
  rails, step-free bedroom/bathroom access, etc. — grouped the same way as amenities, with
  an `available` flag on each item so *unavailable* features are reported as `false` rather
  than dropped. The enrichment POST now requests the accessibility section explicitly (it's
  gated by the amenities fragment we already include). This is a field most competing Airbnb
  scrapers don't expose and is valuable for accessibility-filtered search and compliance use
  cases. Only present on detail runs (`skipDetailPages: false`); omitted when a listing has
  no accessibility data, never shown as misleadingly empty.

#### 0.1.14

- **Results now stream out as the search finds them.** Previously, on large/split-heavy
  runs the actor gathered the *entire* search before emitting anything, so the results
  count sat at 0 for several minutes and then every listing appeared at once — which reads
  as "stuck" while watching a run. Listings are now processed and pushed the moment each
  one is discovered, so output starts within seconds and climbs steadily. Long searches
  also log periodic progress (`N listings found so far…`). Dedup and the `maxListings` cap
  stay exact (each id is reserved synchronously before its work is dispatched), and a
  fetch failure partway through a search now still keeps the listings already emitted
  instead of discarding them. Default-size runs (≤ a few hundred) behave as before.
- Known follow-up (not in this build): the search itself still fetches pages
  sequentially, so very large runs are still bound by per-page latency. Parallelizing
  search-page fetching is a separate, carefully-tested change (it raises block risk) and
  is intentionally deferred.

#### 0.1.13

- **Cap-splitting now triggers on an exhausted page budget, not just an explicit cap hit.**
  A bare location search (e.g. "Lisbon") runs Airbnb's page cursors out at ~15 pages /
  \~240–270 results with `hasNextPage:false`, so the old logic saw "natural end" and never
  split — capping large markets at ~243 regardless of `maxListings`. Now, consuming the
  full page budget is itself a split signal, so price-band splitting fires and the run can
  climb toward the requested total. Small markets (which end before the budget) are
  unaffected and don't over-split.

#### 0.1.12

- **Request rate limiting.** The actor now caps how many requests *start* per minute
  (`maxRequestsPerMinute`, default 120) on top of the concurrency cap, so large/scheduled
  runs stay polite and avoid anti-bot throttling even if `maxConcurrency` is raised. At
  the observed throughput it doesn't slow normal runs; it's a safety ceiling for scale.

#### 0.1.11

- **Photo gallery, full description, and cancellation policy** now enriched via the same
  `StaysPdpSections` mechanism, using the exact section IDs the discovery log surfaced
  (`PHOTO_TOUR_SCROLLABLE_MODAL`, `DESCRIPTION_MODAL`, `CANCELLATION_POLICY_PICKER_MODAL`).
  The enrichment now triggers whenever *any* of amenities / photos / description /
  cancellation is missing (not just amenities), and the parsers pick the section that
  actually carries content (e.g. the full-text description modal over the short default,
  the scrollable gallery over the hero, the cancellation modal over the house-rules
  policies section). Merge stays content-aware — stubs are replaced, richer base data is
  never overwritten.

#### 0.1.10

- **Amenities confirmed working live** (20/20 listings, ~40 amenities each; highlights
  19/20) with no regressions, $0.032, 33s. PII re-checked clean (host carries no contact
  fields; reviews use an opaque reviewer id; no emails/phones).
- Added a one-line **PDP section discovery log** at the end of a run (`PDP sections seen
  this run: …`) so the exact section IDs of not-yet-captured content (the lazy-loaded
  photo gallery, cancellation policy) can be identified and requested precisely rather
  than guessed.

#### 0.1.9

- **Fixed the silent amenities skip.** Airbnb's SSR ships an *empty* amenities section
  stub; the enrichment was treating "section exists" as "amenities present" and skipping
  the `StaysPdpSections` POST entirely (no log, amenities empty every run). The check now
  requires real amenities *content*, and the merge *replaces* an SSR stub with the richer
  GraphQL section (while never overwriting a richer base section with a thinner one). The
  content test is shared with the parser so they can't drift. With this, the POST actually
  runs — so a run now either populates amenities or logs exactly why the POST was rejected.

#### 0.1.8

- **Concurrency.** Listings now process in parallel up to `maxConcurrency` (default 8)
  instead of one at a time, cutting wall-clock time substantially. Ids are reserved
  synchronously before any network call, so the `maxListings` cap and de-duplication
  stay exact under parallelism (covered by tests). If you ever see blocks, lower
  `maxConcurrency`.
- **Amenities enrichment now logs why it failed** (once per run) instead of failing
  silently — e.g. a rejected request or a response with no amenities section — so the
  cause is visible in the log.

#### 0.1.7

- **Amenities & highlights now captured.** They're lazy-loaded and absent from the
  page's SSR state, so when detail is on the Actor makes a `StaysPdpSections` GraphQL
  POST (verified request shape + section IDs) and **merges** the missing sections
  (`AMENITIES_DEFAULT`, `HIGHLIGHTS_DEFAULT`, full description) into the listing —
  merge-only, so existing SSR fields (house rules, host, room/property type) can't
  regress. Best-effort: if the call fails, the listing keeps its SSR data unchanged.
  The run summary reports how many listings were enriched.

#### 0.1.6

- **Reviews & calendar now run over cheap HTTP GraphQL — no browser.** Root cause of
  the expensive run found from a live request capture: the reviews operation was named
  `StaysPdpReviews` but Airbnb's real operation is **`StaysPdpReviewsQuery`**, so the
  GraphQL path never matched and every listing fell back to Playwright (the costly
  tier). Fixed the operation name, corrected the reviews/calendar request variables to
  the verified live shape, and **seeded the known persisted-query hashes** so GraphQL
  works from the first call. Playwright remains a self-healing fallback if Airbnb
  rotates a hash. Expected effect: per-listing browser launches drop to ~zero, with a
  large cost/time reduction on detail+reviews runs.

#### 0.1.5

- **Reviews work** (verified on a live run: 419 reviews across 20 listings, no crashes
  or timeouts) thanks to the 0.1.4 Playwright fix.
- **Cost reduction (reviews):** each review fetch previously launched a browser
  (Playwright is the expensive tier — it drove ~all of a $1.18 / 8.5-min run). Now a
  single browser load **harvests the persisted-query hash** for reviews/sections/
  calendar, so subsequent fetches use cheap HTTP GraphQL. Falls back to Playwright if
  the GraphQL replay fails (no regression). Net browser launches should drop sharply.
- **`maxReviewsPerListing` now defaults to 50** (was unlimited) to bound cost on
  listings with hundreds of reviews; set `0` for the full set.

#### 0.1.4

- **Fix: Playwright fallback no longer hangs.** It used `waitUntil: 'networkidle'`
  with a 90s timeout; Airbnb never reaches networkidle, so every fallback timed out
  (a reviews run aborted at 7 min with only 4 listings). Now uses `domcontentloaded`
  (45s cap), treats slow navigation as non-fatal, scrolls to trigger lazy content, and
  waits a bounded 12s for the target XHR — and preserves any response intercepted
  during load.
- **Reviews circuit breaker.** After 3 consecutive review-fetch failures, reviews are
  disabled for the rest of the run (LOUD warning); listings still emit without reviews.
  Prevents one broken path from turning N listings into N slow failures.
- **Clearer detail toggle.** Reworded `skipDetailPages` to remove the double-negative
  confusion: checked = skip (fast), unchecked = full details.
- Note: full amenities/highlights are lazy-loaded by Airbnb and may be absent from the
  fast SSR detail path; capturing them reliably is a pending fetch-strategy decision
  (GraphQL sections vs Playwright).

#### 0.1.3

- **Detail pages now OFF by default** (`skipDetailPages: true`). The default run is
  fast, cheap search-only; full per-listing detail is an explicit opt-in
  (`skipDetailPages: false`). A directly-supplied listing URL is still fetched in full
  regardless of the flag, so single-listing requests are never near-empty.
- **`maxListings` default is now 100** (was unlimited). Predictable cost/time out of the
  box; raise it for bigger pulls or set `0` for an explicit unlimited crawl.

#### 0.1.2

- **Fix: detail-page parse crash.** `parseRegistration` called `.toLowerCase()` on a
  PDP section title that Airbnb now returns as an object, throwing a `TypeError` for
  almost every listing when detail pages were enabled. Section title/heading are now
  string-coerced first. (Detail-page data was being discarded for ~96% of listings as
  a result.)
- **Fix: inverted discount price.** A two-figure price label was split as
  original=first / discounted=last, which produced an `originalPrice` *below* the
  current price. Discount handling now (a) only triggers on an explicit discount
  signal (was/now/originally/…), and (b) assigns discounted = lower figure, original =
  higher figure regardless of token order. Without a discount signal, only the headline
  price is emitted (no fabricated discount).
- **Added `pricing.priceLabel`** — the exact displayed price string, for transparency
  and to confirm the real label format from live runs.

#### 0.1.1

- **Fix: zero-result searches on a populated market.** Airbnb moved each search
  result from `result.listing` (numeric `id`) to `result.demandStayListing`
  (base64 `"DemandStayListing:<id>"`), with rating now in `avgRatingA11yLabel`
  as a string. The search parser was dropping every card. It now decodes the
  base64 id, reads the current `demandStayListing` / `structuredContent` /
  `structuredDisplayPrice` paths, and remains backward‑compatible with the old
  shape.
- **New fields:** `isGuestFavorite` (boolean) and `badges` (string array), surfaced
  from result badges. Title is taken from `structuredContent.primaryLine`; the
  host‑bearing `secondaryLine` ("Stay with …") is never emitted (public‑data rule).
- **New status `error:parserStale`.** If result items are located but none parse,
  the run no longer reports a false `empty:zero_results`. It emits an explained,
  un‑charged fault, logs a LOUD rot warning, and the run flags rather than crashes.
- **Cost:** on a parse miss the collector now bails after one page instead of
  paginating the full 15‑page budget and price‑band splitting (≈14 fewer proxy
  requests per affected search).

# Actor input Schema

## `locationQueries` (type: `array`):

Free-text locations to search, e.g. "London", "Miami Beach, FL", "Lisbon, Portugal". Each becomes an Airbnb search.

## `searchUrls` (type: `array`):

Pre-filtered Airbnb search result URLs (paste from your browser after applying filters not exposed below, e.g. instant book, specific amenities). One URL per line.

## `listingUrls` (type: `array`):

Direct Airbnb property/room URLs (e.g. https://www.airbnb.com/rooms/12345678). Scraped as individual properties — no separate Actor needed.

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

Check-in date. Affects price and availability. Leave empty for undated search.

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

Check-out date. Must be after check-in.

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

Number of adult guests.

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

Number of children (ages 2–12).

## `infants` (type: `integer`):

Number of infants (under 2).

## `pets` (type: `integer`):

Number of pets.

## `priceMin` (type: `integer`):

Minimum nightly price in the chosen currency.

## `priceMax` (type: `integer`):

Maximum nightly price in the chosen currency.

## `minBeds` (type: `integer`):

Minimum number of beds.

## `minBedrooms` (type: `integer`):

Minimum number of bedrooms.

## `minBathrooms` (type: `integer`):

Minimum number of bathrooms.

## `propertyTypes` (type: `array`):

Filter by Airbnb room type. Allowed values: "Entire home/apt", "Private room", "Shared room", "Hotel room". Unknown values are ignored.

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

Currency for all prices.

## `locale` (type: `string`):

Language/locale for content.

## `skipDetailPages` (type: `boolean`):

Default is OFF (unchecked) = scrape FULL detail for every listing: description, amenities, accessibility, house rules, registration number, full photo gallery, and host data (id, Superhost, verified, rating, time as host, response rate/time) — the richest output. Turn this ON (checked) for a fast, cheap SEARCH-ONLY run that returns just card data: title, price, rating, coordinates, badges, Superhost/Guest-favorite, thumbnail (~1 fetch per ~18 listings, a full city in ~1-2 min). A single listing URL you pass directly is always fetched in full regardless of this toggle.

## `calendarMonths` (type: `integer`):

Months of future availability to fetch per listing (0 = none). Each month is one extra request per listing. Returns per-day availability (date, available, min/max nights, check-in/out eligibility). Per-day NIGHTLY PRICE is a separate opt-in below ('Calendar nightly prices') because Airbnb's calendar endpoint carries no prices — each priced day needs its own quote request.

## `calendarPrices` (type: `boolean`):

TEMPORARILY UNAVAILABLE until ~July 10, 2026 while per-day pricing is finalized — enabling it currently has no effect (the calendar returns availability only). When it returns: it fetches the nightly rate for each available calendar day. Airbnb does NOT expose per-day prices in bulk, so each priced day requires its own price-quote request (one request per available day per listing) and will be billed as a separate 'calendar-price-day' event in addition to the per-listing charge. Requires 'Calendar months' >= 1; capped by 'Max priced calendar days'.

## `maxCalendarPriceDays` (type: `integer`):

When 'Calendar nightly prices' is on, cap how many available days per listing are price-quoted (0 = unlimited). Defaults to 31 (about one month) to bound cost/time.

## `scrapeReviews` (type: `boolean`):

Default is ON. Fetch reviews (text, rating, date, language, opaque reviewer ID only — never reviewer name/photo/profile). Turn off to skip reviews for a lighter, cheaper run.

## `maxReviewsPerListing` (type: `integer`):

Cap reviews fetched per listing. Defaults to 50 to bound cost/time on listings with hundreds of reviews; set 0 for unlimited (Airbnb's full set). Only applies when 'Scrape reviews' is on.

## `maxListings` (type: `integer`):

Stop after this many unique listings across all sources. Defaults to 100 to keep runs fast and costs predictable; raise it for bigger pulls. Set 0 for unlimited (a dense city can return many thousands via price-band splitting — only use 0 if you intend a full, potentially large/expensive crawl).

## `priceSplitCeiling` (type: `integer`):

Upper price bound for the price-band splitting algorithm when a search hits the ~270 cap (NOT a listing filter). 0 = automatic. Higher values probe more of the long price tail.

## `enableMapSplit` (type: `boolean`):

Recursively subdivide the map viewport into quadrants when price-band splitting still hits the cap. Useful for dense cities.

## `incrementalMode` (type: `boolean`):

Track a market over time. First run captures a baseline; later runs emit only changes (new / price / availability / rating / delisted). Requires a Monitor key.

## `monitorKey` (type: `string`):

Stable name for the market you are monitoring (e.g. "lisbon-2br"). State is persisted in a named Key-Value store across runs. Required when monitoring is on.

## `trackPriceChanges` (type: `boolean`):

Emit priceChanged records with old → new price and delta.

## `trackAvailabilityChanges` (type: `boolean`):

Emit availabilityChanged records when a listing opens/closes for the monitored dates.

## `detectDelistings` (type: `boolean`):

After scanning the whole market, emit delisted records for listings seen before but now absent.

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

Apify Proxy settings. Defaults to fast, low-cost datacenter proxies with automatic fallback to residential when Airbnb blocks datacenter — the cheapest option that still works reliably. For maximum reliability you can force residential by selecting the RESIDENTIAL group.

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

Maximum parallel requests. Keep conservative (5–10) to avoid blocks.

## `maxRequestsPerMinute` (type: `integer`):

Global request rate cap (throttling helps avoid 429s).

## `maxRequestRetries` (type: `integer`):

Retries per request before a listing/search is classified and skipped (the run does not fail).

## Actor input object example

```json
{
  "locationQueries": [
    "Lisbon, Portugal"
  ],
  "adults": 1,
  "children": 0,
  "infants": 0,
  "pets": 0,
  "currency": "USD",
  "locale": "en",
  "skipDetailPages": false,
  "calendarMonths": 0,
  "calendarPrices": false,
  "maxCalendarPriceDays": 31,
  "scrapeReviews": true,
  "maxReviewsPerListing": 50,
  "maxListings": 100,
  "priceSplitCeiling": 0,
  "enableMapSplit": false,
  "incrementalMode": false,
  "trackPriceChanges": true,
  "trackAvailabilityChanges": true,
  "detectDelistings": true,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "maxConcurrency": 8,
  "maxRequestsPerMinute": 120,
  "maxRequestRetries": 5
}
```

# Actor output Schema

## `listings` (type: `string`):

All scraped listings and the run-summary record. The Overview view shows key fields; switch to All fields for the complete record (amenities, photos, host data, reviews, etc.).

# 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 = {
    "locationQueries": [
        "Lisbon, Portugal"
    ],
    "adults": 1,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapesage/airbnb-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 = {
    "locationQueries": ["Lisbon, Portugal"],
    "adults": 1,
    "proxyConfiguration": { "useApifyProxy": True },
}

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

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

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

```

## CLI example

```bash
echo '{
  "locationQueries": [
    "Lisbon, Portugal"
  ],
  "adults": 1,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call scrapesage/airbnb-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=scrapesage/airbnb-scraper",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "Airbnb Scraper & Market Monitor",
        "description": "Reliable Airbnb scraper: location/URL search, property details, availability calendar (with prices), and reviews in one Actor — plus an incremental monitoring mode that tracks new listings, price changes, availability changes and delistings over time. Public listing & host-operational data only.",
        "version": "0.1",
        "x-build-id": "4uLvBdE8FHIMB1yz8"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/scrapesage~airbnb-scraper/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-scrapesage-airbnb-scraper",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/scrapesage~airbnb-scraper/runs": {
            "post": {
                "operationId": "runs-sync-scrapesage-airbnb-scraper",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/scrapesage~airbnb-scraper/run-sync": {
            "post": {
                "operationId": "run-sync-scrapesage-airbnb-scraper",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "properties": {
                    "locationQueries": {
                        "title": "Location queries",
                        "type": "array",
                        "description": "Free-text locations to search, e.g. \"London\", \"Miami Beach, FL\", \"Lisbon, Portugal\". Each becomes an Airbnb search.",
                        "items": {
                            "type": "string"
                        }
                    },
                    "searchUrls": {
                        "title": "Search URLs",
                        "type": "array",
                        "description": "Pre-filtered Airbnb search result URLs (paste from your browser after applying filters not exposed below, e.g. instant book, specific amenities). One URL per line.",
                        "items": {
                            "type": "string"
                        }
                    },
                    "listingUrls": {
                        "title": "Listing URLs",
                        "type": "array",
                        "description": "Direct Airbnb property/room URLs (e.g. https://www.airbnb.com/rooms/12345678). Scraped as individual properties — no separate Actor needed.",
                        "items": {
                            "type": "string"
                        }
                    },
                    "checkIn": {
                        "title": "Check-in (YYYY-MM-DD)",
                        "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
                        "type": "string",
                        "description": "Check-in date. Affects price and availability. Leave empty for undated search."
                    },
                    "checkOut": {
                        "title": "Check-out (YYYY-MM-DD)",
                        "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
                        "type": "string",
                        "description": "Check-out date. Must be after check-in."
                    },
                    "adults": {
                        "title": "Adults",
                        "minimum": 0,
                        "type": "integer",
                        "description": "Number of adult guests.",
                        "default": 1
                    },
                    "children": {
                        "title": "Children",
                        "minimum": 0,
                        "type": "integer",
                        "description": "Number of children (ages 2–12).",
                        "default": 0
                    },
                    "infants": {
                        "title": "Infants",
                        "minimum": 0,
                        "type": "integer",
                        "description": "Number of infants (under 2).",
                        "default": 0
                    },
                    "pets": {
                        "title": "Pets",
                        "minimum": 0,
                        "type": "integer",
                        "description": "Number of pets.",
                        "default": 0
                    },
                    "priceMin": {
                        "title": "Min price (per night)",
                        "minimum": 0,
                        "type": "integer",
                        "description": "Minimum nightly price in the chosen currency."
                    },
                    "priceMax": {
                        "title": "Max price (per night)",
                        "minimum": 0,
                        "type": "integer",
                        "description": "Maximum nightly price in the chosen currency."
                    },
                    "minBeds": {
                        "title": "Min beds",
                        "minimum": 0,
                        "type": "integer",
                        "description": "Minimum number of beds."
                    },
                    "minBedrooms": {
                        "title": "Min bedrooms",
                        "minimum": 0,
                        "type": "integer",
                        "description": "Minimum number of bedrooms."
                    },
                    "minBathrooms": {
                        "title": "Min bathrooms",
                        "minimum": 0,
                        "type": "integer",
                        "description": "Minimum number of bathrooms."
                    },
                    "propertyTypes": {
                        "title": "Room / property types",
                        "type": "array",
                        "description": "Filter by Airbnb room type. Allowed values: \"Entire home/apt\", \"Private room\", \"Shared room\", \"Hotel room\". Unknown values are ignored.",
                        "items": {
                            "type": "string"
                        }
                    },
                    "currency": {
                        "title": "Currency",
                        "enum": [
                            "USD",
                            "EUR",
                            "GBP",
                            "CAD",
                            "AUD",
                            "JPY",
                            "CNY",
                            "INR",
                            "BRL",
                            "MXN",
                            "CHF",
                            "SEK",
                            "NOK",
                            "DKK",
                            "PLN",
                            "ZAR",
                            "AED",
                            "SGD",
                            "HKD",
                            "NZD"
                        ],
                        "type": "string",
                        "description": "Currency for all prices.",
                        "default": "USD"
                    },
                    "locale": {
                        "title": "Locale",
                        "enum": [
                            "en",
                            "en-GB",
                            "fr",
                            "de",
                            "es",
                            "it",
                            "pt",
                            "nl",
                            "ja",
                            "zh",
                            "ru",
                            "ko",
                            "pl",
                            "sv",
                            "da",
                            "nb"
                        ],
                        "type": "string",
                        "description": "Language/locale for content.",
                        "default": "en"
                    },
                    "skipDetailPages": {
                        "title": "Skip detail pages (faster, less data)",
                        "type": "boolean",
                        "description": "Default is OFF (unchecked) = scrape FULL detail for every listing: description, amenities, accessibility, house rules, registration number, full photo gallery, and host data (id, Superhost, verified, rating, time as host, response rate/time) — the richest output. Turn this ON (checked) for a fast, cheap SEARCH-ONLY run that returns just card data: title, price, rating, coordinates, badges, Superhost/Guest-favorite, thumbnail (~1 fetch per ~18 listings, a full city in ~1-2 min). A single listing URL you pass directly is always fetched in full regardless of this toggle.",
                        "default": false
                    },
                    "calendarMonths": {
                        "title": "Calendar months",
                        "minimum": 0,
                        "maximum": 12,
                        "type": "integer",
                        "description": "Months of future availability to fetch per listing (0 = none). Each month is one extra request per listing. Returns per-day availability (date, available, min/max nights, check-in/out eligibility). Per-day NIGHTLY PRICE is a separate opt-in below ('Calendar nightly prices') because Airbnb's calendar endpoint carries no prices — each priced day needs its own quote request.",
                        "default": 0
                    },
                    "calendarPrices": {
                        "title": "Calendar nightly prices (per day) — temporarily unavailable until ~July 10, 2026",
                        "type": "boolean",
                        "description": "TEMPORARILY UNAVAILABLE until ~July 10, 2026 while per-day pricing is finalized — enabling it currently has no effect (the calendar returns availability only). When it returns: it fetches the nightly rate for each available calendar day. Airbnb does NOT expose per-day prices in bulk, so each priced day requires its own price-quote request (one request per available day per listing) and will be billed as a separate 'calendar-price-day' event in addition to the per-listing charge. Requires 'Calendar months' >= 1; capped by 'Max priced calendar days'.",
                        "default": false
                    },
                    "maxCalendarPriceDays": {
                        "title": "Max priced calendar days (per listing)",
                        "minimum": 0,
                        "maximum": 366,
                        "type": "integer",
                        "description": "When 'Calendar nightly prices' is on, cap how many available days per listing are price-quoted (0 = unlimited). Defaults to 31 (about one month) to bound cost/time.",
                        "default": 31
                    },
                    "scrapeReviews": {
                        "title": "Scrape reviews",
                        "type": "boolean",
                        "description": "Default is ON. Fetch reviews (text, rating, date, language, opaque reviewer ID only — never reviewer name/photo/profile). Turn off to skip reviews for a lighter, cheaper run.",
                        "default": true
                    },
                    "maxReviewsPerListing": {
                        "title": "Max reviews per listing",
                        "minimum": 0,
                        "type": "integer",
                        "description": "Cap reviews fetched per listing. Defaults to 50 to bound cost/time on listings with hundreds of reviews; set 0 for unlimited (Airbnb's full set). Only applies when 'Scrape reviews' is on.",
                        "default": 50
                    },
                    "maxListings": {
                        "title": "Max listings",
                        "minimum": 0,
                        "type": "integer",
                        "description": "Stop after this many unique listings across all sources. Defaults to 100 to keep runs fast and costs predictable; raise it for bigger pulls. Set 0 for unlimited (a dense city can return many thousands via price-band splitting — only use 0 if you intend a full, potentially large/expensive crawl).",
                        "default": 100
                    },
                    "priceSplitCeiling": {
                        "title": "Price-split ceiling (advanced)",
                        "minimum": 0,
                        "type": "integer",
                        "description": "Upper price bound for the price-band splitting algorithm when a search hits the ~270 cap (NOT a listing filter). 0 = automatic. Higher values probe more of the long price tail.",
                        "default": 0
                    },
                    "enableMapSplit": {
                        "title": "Enable map splitting (dense areas)",
                        "type": "boolean",
                        "description": "Recursively subdivide the map viewport into quadrants when price-band splitting still hits the cap. Useful for dense cities.",
                        "default": false
                    },
                    "incrementalMode": {
                        "title": "Incremental monitoring mode",
                        "type": "boolean",
                        "description": "Track a market over time. First run captures a baseline; later runs emit only changes (new / price / availability / rating / delisted). Requires a Monitor key.",
                        "default": false
                    },
                    "monitorKey": {
                        "title": "Monitor key",
                        "type": "string",
                        "description": "Stable name for the market you are monitoring (e.g. \"lisbon-2br\"). State is persisted in a named Key-Value store across runs. Required when monitoring is on."
                    },
                    "trackPriceChanges": {
                        "title": "Track price changes",
                        "type": "boolean",
                        "description": "Emit priceChanged records with old → new price and delta.",
                        "default": true
                    },
                    "trackAvailabilityChanges": {
                        "title": "Track availability changes",
                        "type": "boolean",
                        "description": "Emit availabilityChanged records when a listing opens/closes for the monitored dates.",
                        "default": true
                    },
                    "detectDelistings": {
                        "title": "Detect delistings",
                        "type": "boolean",
                        "description": "After scanning the whole market, emit delisted records for listings seen before but now absent.",
                        "default": true
                    },
                    "proxyConfiguration": {
                        "title": "Proxy configuration",
                        "type": "object",
                        "description": "Apify Proxy settings. Defaults to fast, low-cost datacenter proxies with automatic fallback to residential when Airbnb blocks datacenter — the cheapest option that still works reliably. For maximum reliability you can force residential by selecting the RESIDENTIAL group.",
                        "default": {
                            "useApifyProxy": true
                        }
                    },
                    "maxConcurrency": {
                        "title": "Max concurrency",
                        "minimum": 1,
                        "maximum": 50,
                        "type": "integer",
                        "description": "Maximum parallel requests. Keep conservative (5–10) to avoid blocks.",
                        "default": 8
                    },
                    "maxRequestsPerMinute": {
                        "title": "Max requests per minute",
                        "minimum": 1,
                        "type": "integer",
                        "description": "Global request rate cap (throttling helps avoid 429s).",
                        "default": 120
                    },
                    "maxRequestRetries": {
                        "title": "Max request retries",
                        "minimum": 0,
                        "maximum": 15,
                        "type": "integer",
                        "description": "Retries per request before a listing/search is classified and skipped (the run does not fail).",
                        "default": 5
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
