# Airbnb Market Tracker — Occupancy, ADR & Booking Pace (`signalbench/airbnb-market-tracker`) Actor

Track Airbnb markets over time. Diffs each listing's availability calendar day over day to surface booking pace, lead time, an occupancy proxy and true ADR from real quotes, per listing and rolled up per market. No browser.

- **URL**: https://apify.com/signalbench/airbnb-market-tracker.md
- **Developed by:** [SignalBench](https://apify.com/signalbench) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 tracked rows

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

## Airbnb Market Tracker — Occupancy, ADR & Booking Pace

Listing scrapers tell you what exists. This one tells you **what changed, and what it implies about the market.**

Run it on a schedule. Every run reads each listing's availability calendar and diffs it against the last run. Nights that flipped from available to unavailable came off the market — and that single diff produces the numbers STR operators actually pay for.

### What you get

**Per listing, per day**

| Field | Meaning |
|---|---|
| `nightsTakenSinceLast` | The exact dates that came off the market since the last run |
| `takeRatePerDay` | Nights taken per day elapsed — booking pace |
| `avgLeadTimeDays` | How far ahead those nights sit — are they filling next week or next season? |
| `unavailableShare30/60/90` | Share of the forward window that's blocked — the occupancy proxy |
| `adr`, `adrDelta`, `adrPctChange` | True nightly rate from a real quote, and how it moved |
| `nightsReleasedSinceLast` | Cancellations, or a host reopening dates |
| `reviewCountDelta`, `ratingDelta` | Review velocity and rating drift |
| `badgesGained` / `badgesLost` | Superhost and Guest Favorite changes |

**Per market, per day**

`activeListings`, `newListings`, `delistings`, `medianAdr` with `adrP25`/`adrP75`, `medianUnavailableShare30/60/90`, `totalNightsTaken`, `bookingPaceIndex`, `medianLeadTimeDays`, `superhostShare`, `guestFavoriteShare`, `medianMinNights`, and the day's `topMovers`.

Real output from a single run, two Tennessee markets:

| Market | Listings | Median ADR | P25–P75 | Blocked (30d) |
|---|---|---|---|---|
| Chattanooga, TN | 9 | $270 | $167 – $568 | 63% |
| Gatlinburg, TN | 5 | $1,056 | $1,000 – $1,112 | 87% |

That contrast — a city market against a resort market — is the shape of the signal.

### About that ADR

Airbnb publishes **no per-night price** anywhere on a listing page or in its calendar. The only honest nightly rate is one taken from an actual quote, so with `pricing: "exact"` the Actor prices a real stay and reads back the true nightly rate, the fees and the taxes a guest would pay.

Because it quotes a real stay, keep `priceProbeStayNights` and `priceProbeLeadDays` **constant across runs** — change them and your ADR time series stops being comparable. A fully-booked listing with no open window returns `adr: null`; `listingsWithPricing` tells you how many listings the market's ADR figures are actually based on.

### The one thing you must not oversell

**A blocked night is not proof of a booking.** A host closing dates for their own use is indistinguishable from a guest booking them — Airbnb exposes nothing that separates the two. That's why every field here is named for what it measures: `nightsTaken`, not "nights booked"; `unavailableShare`, not "occupancy"; `bookingPaceIndex`, not "bookings per day".

Directionally these track real demand well, and across a whole market the host-blocking noise largely averages out. Just don't put "occupancy: 87%" in front of a client as a fact.

### Cold start

**The first run is worth nothing.** It has nothing to diff against, so every delta comes back `null` and `hasComparisonBaseline` is `false` — deliberately null rather than zero, because zero would read as "nothing moved".

Value accrues from run two onward. Schedule it daily and let it build. Pickup curves and seasonal patterns need weeks of history that cannot be backfilled — Airbnb does not publish the past.

### Cost control

Each tracked listing costs 2 requests (page + calendar), or 3 with `pricing: "exact"`. Multiply that by your listing count and your schedule before pointing it at a whole city.

The levers that actually cut cost: `forwardWindowDays` (90 is plenty for near-term pace; 365 is for seasonality work), `maxListingsPerMarket`, and turning pricing off on runs where you only need availability.

A note on the ceiling: Airbnb publishes twelve calendar months starting from the *first of the current month*, so asking for 365 nights returns whatever remains from today — around 355 mid-month, rising back to 365 on the 1st. That is Airbnb's limit, not a truncation bug.

Measured throughput: 60 listings with pricing on (about 180 requests) completed in roughly three minutes with no rate limiting.

### Stable cohorts

Airbnb reshuffles search results constantly. If the tracked set were re-derived from search every run, listings would drop in and out, day-over-day deltas would compare different properties, and ordinary ranking churn would show up as delistings.

So once a listing enters your tracked set it stays measured, whether or not today's search returns it (`trackKnownListings`, on by default). `isDelisted` is only ever set when the Actor asked Airbnb for that listing directly and got nothing back.

### Storage

Alongside the run's own dataset, history is mirrored into named datasets that accumulate across runs — `str-snapshots`, `str-movers`, `str-markets` — and the diffing state lives in the `str-state` key-value store. Mirrored rows are never charged; only the run's own dataset is. Use separate `stateStoreName` values to track separate cohorts independently.

### Comp sets

`includeCompSet` attaches the nearest similar-sized listings from the same tracked market, with their ADR and occupancy proxy.

To be clear about provenance: this is a comp set **this Actor derives** from your tracked market by proximity and bedroom count. Airbnb's own similar-listings carousel did not render on any listing sampled in September 2026, so there is no vendor comp set to read. The output labels it `compSetSource: "derived-from-market"` so nobody downstream mistakes it for Airbnb's opinion.

### Keeping working when Airbnb deploys

Airbnb's internal API uses persisted queries whose hashes rotate on every deploy. Nothing here is hard-coded and trusted: at the start of each run the Actor re-reads the operation-name → hash map from Airbnb's own JS bundles and adopts what it finds. The run summary reports which hashes were confirmed, which had rotated, and which could not be verified. An operation that can't be resolved degrades to a null field rather than silently returning wrong data.

### Pricing

**$3.00 per 1,000 rows**, pay-per-event. One stored row — a snapshot, a delta, or a market rollup — is one charge. Listings that return nothing are never stored and never billed.

### Responsible use

Airbnb's Terms of Service prohibit scraping; read them and decide for yourself. This Actor stays off the paths `robots.txt` disallows — it never touches `/calendar/`, `/book/`, `/manage-listing` or the disallowed `/rooms/*/` sub-pages, reading availability through the same public API the listing page itself calls. Note that `robots.txt` does disallow `/s/*/*`, the search path used to discover listings in a market; `listingIds` mode avoids it entirely.

Requests are paced and retried politely. Tracking runs are large and repeat daily, so raise `politenessDelayMs` rather than risk rate limiting.

# Actor input Schema

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

"Snapshot" writes today's state of every listing. "Deltas" writes what changed per listing since the last run — this is the core output, and it needs at least two runs to mean anything. "Market rollup" writes one aggregate row per market.

## `markets` (type: `array`):

Places to track — a city name like "Gatlinburg, TN", or a full Airbnb search URL if you want to carry filters (price band, property type, bedrooms) into the tracked set. Leave empty if you are tracking specific listings instead.

## `listingIds` (type: `array`):

Optional. Airbnb listing URLs or bare numeric IDs to track regardless of any market. Use this to follow your own portfolio.

## `maxListingsPerMarket` (type: `integer`):

How many listings to pull from each market's search results. Previously tracked listings are carried forward on top of this, so the cohort you measure stays stable.

## `forwardWindowDays` (type: `integer`):

How many nights ahead to read each calendar. Airbnb caps this at 365. A shorter window is cheaper and still captures near-term booking pace; a longer one is needed for seasonal pickup curves.

## `pricing` (type: `string`):

"Exact" quotes a real stay per listing to get the true nightly rate plus fees and taxes — Airbnb publishes no per-night price on the calendar, so this is the only accurate ADR. It costs one extra request per listing. "Off" skips it and leaves ADR null.

## `priceProbeLeadDays` (type: `integer`):

How far ahead to look for the stay to quote. The Actor picks the first bookable window at or after this many days out, so a busy listing still returns a rate.

## `priceProbeStayNights` (type: `integer`):

Length of the stay to quote. Keep this consistent across runs or your ADR series will not be comparable.

## `includeCompSet` (type: `boolean`):

Attach the nearest similar-sized listings from the same market to each snapshot row, with their ADR and occupancy proxy. Note this is a comp set derived from your tracked market, not one published by Airbnb.

## `compSetSize` (type: `integer`):

How many comparable listings to attach to each snapshot row.

## `includeNightlyCalendar` (type: `boolean`):

Attach every night's availability to snapshot rows. Off by default because it makes rows large; the summary counts and the deltas are computed either way.

## `trackKnownListings` (type: `boolean`):

Airbnb reshuffles search constantly. With this on, a listing already in your tracked set is still measured even when today's search does not return it — which keeps day-over-day deltas comparing the same properties and stops ranking churn being reported as delistings.

## `stateStoreName` (type: `string`):

Named key-value store holding the previous run's calendars, used for diffing. Use separate names to track separate cohorts independently.

## `archiveDatasets` (type: `boolean`):

Mirror output into the named datasets str-snapshots, str-movers and str-markets so history accumulates across runs. These mirrors are never charged — only the run's own dataset is.

## `excludePersonalData` (type: `boolean`):

Drop host names and profile IDs. Market aggregates do not need host identity at all.

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

ISO currency code for all quoted prices, e.g. USD, EUR, GBP.

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

Language for listing titles and labels, e.g. en, es, fr, de.

## `politenessDelayMs` (type: `integer`):

Randomised pause between requests. Tracking runs are large and repeat daily — raise this rather than risk rate limiting.

## `maxRetries` (type: `integer`):

How many times to retry a blocked or failed request, rotating the proxy session each time.

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

Optional. Airbnb serves this data without a bot-detection stack, so a direct connection normally works. Consider a proxy for large daily runs.

## Actor input object example

```json
{
  "mode": "snapshot",
  "markets": [
    "Gatlinburg, TN"
  ],
  "maxListingsPerMarket": 25,
  "forwardWindowDays": 90,
  "pricing": "off",
  "priceProbeLeadDays": 30,
  "priceProbeStayNights": 3,
  "includeCompSet": false,
  "compSetSize": 5,
  "includeNightlyCalendar": false,
  "trackKnownListings": true,
  "stateStoreName": "str-state",
  "archiveDatasets": true,
  "excludePersonalData": false,
  "currency": "USD",
  "locale": "en",
  "politenessDelayMs": 1000,
  "maxRetries": 3
}
```

# Actor output Schema

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

This run's rows in the mode you selected.

## `resultsCsv` (type: `string`):

The same rows flattened for spreadsheets.

## `summary` (type: `string`):

Markets covered, rows stored, failures, and which query hashes were re-verified this run.

## `viewInConsole` (type: `string`):

No description

# API

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

## JavaScript example

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

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

// Prepare Actor input
const input = {
    "markets": [
        "Gatlinburg, TN"
    ],
    "maxListingsPerMarket": 25
};

// Run the Actor and wait for it to finish
const run = await client.actor("signalbench/airbnb-market-tracker").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 = {
    "markets": ["Gatlinburg, TN"],
    "maxListingsPerMarket": 25,
}

# Run the Actor and wait for it to finish
run = client.actor("signalbench/airbnb-market-tracker").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 '{
  "markets": [
    "Gatlinburg, TN"
  ],
  "maxListingsPerMarket": 25
}' |
apify call signalbench/airbnb-market-tracker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,signalbench/airbnb-market-tracker"
        }
    }
}
```

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/xId5L92lhlGsqIjWU/builds/0ShseYp11xQweD8aQ/openapi.json
