# Airbnb Market & Nightly Rate Scraper (`eiv/airbnb-market-scraper`) Actor

Scrape Airbnb stays with the real nightly rate, taxes, fees and total as separate figures that add up - plus rating, reviews, capacity and coordinates. Median rate per market. Every breakdown is checked against its own total and withheld rather than guessed. Global. No API key.

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

## Pricing

from $0.80 / 1,000 stay scrapes

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 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 & Nightly Rate Scraper

What a stay **actually costs per night** — with the tax, fee and discount lines that make up the total, plus a median-rate summary for the whole market.

```
Austin, TX          72 stays   median $217.70/night   total $1,258   rating 4.93
Paris, France       72 stays   median $146.13/night   total $  824   rating 4.88
Lisbon, Portugal    72 stays   median $134.24/night   total $  684   rating 4.81

Studio Haus | Short Drive to ACL      5 nights x $284.20 = $1,421.00
+ taxes $241.57  =  $1,662.57 total   ·  4.68 (189 reviews)  ·  1 bedroom, 1 bath
```

**216 stays across three countries in 15 seconds.** No browser, no API key, no login.

***

### The nightly rate is read, not guessed

Most scrapers report the headline price and stop. This one takes Airbnb's own breakdown apart, so you get `nightlyRate`, `accommodationTotal`, `discount`, `fees` and `taxes` as separate numbers that add up to `totalPrice`.

That matters because dividing a total by nights gives a figure nobody is ever quoted — the total already carries cleaning fees, resort fees and tax. **Across 216 live stays, every row reconciled**: `subtotal − discount + fees + taxes = total`.

***

### What you get

**Per stay**

| Group | Fields |
|---|---|
| **Pricing** | **`nightlyRate`**, `nights`, `accommodationTotal`, `discount`, `fees`, `feeLabels`, `taxes`, **`totalPrice`**, `currency` |
| **Quality** | `rating`, `reviewsCount`, `isGuestFavorite`, `isNewListing`, `badges` |
| **Property** | `propertyType`, `bedText`, `bedrooms`, `beds`, `bathText`, `bathrooms`, `isSharedBath` |
| **Location** | `listingCity`, **`latitude`**, **`longitude`** |
| **Identity** | `listingId`, `listingUrl`, `name`, `thumbnailUrl` |

**Per market** — median nightly rate, median total, rate range, median rating and review count, Guest Favourite count, unreviewed-supply count, and the whole-property vs private-room split. Medians throughout: one penthouse moves a mean and not a median.

***

### Who this is for

- **Short-term rental investors** — median $/night by market is the revenue half of a yield model. Pair it with a purchase price and you have a cap rate.
- **Revenue managers** — track your market's median rate weekly and see where you sit in the range.
- **Hosts** — find what comparable listings with your bed count charge on your dates.
- **Travel and analytics teams** — coordinates on every row, stable `listingId` for clean diffs across runs.

***

### Input

```json
{
  "locations": ["Austin, TX", "Paris, France"],
  "checkIn": "2026-10-10",
  "checkOut": "2026-10-15",
  "adults": 2,
  "currency": "USD",
  "maxListingsPerLocation": 180
}
```

A plain place name is enough — `"Austin, TX"`, `"Paris"`, `"Lisbon, Portugal"`. Paste an `airbnb.com/s/...` URL instead if you have already filtered it on the site.

| Option | Default | Notes |
|---|---|---|
| `checkIn` / `checkOut` | — | **Strongly recommended.** See below |
| `adults` / `children` / `infants` / `pets` | 2 / 0 / 0 / 0 | Changes which stays qualify and what they cost |
| `currency` | `USD` | Always sent; see below |
| `roomTypes` | all | entire-home, private-room, shared-room, hotel |
| `minPrice` / `maxPrice` | — | Also the way past the search-depth ceiling — run price bands |
| `minBedrooms` / `minBeds` / `minBathrooms` | — | Applied by Airbnb before billing |
| `superhostOnly` | `false` | |
| `maxListingsPerLocation` | `180` | 270 is Airbnb's hard ceiling per search |
| `maxConcurrency` | `3` | Deliberately low; see below |

***

### Five things worth knowing

Each was found by running against live data.

**Without dates, Airbnb invents the window.** It always quotes something. Left to itself it picks its own stay length and applies its own long-stay discounts, so the same market scraped twice a week apart is not comparable, and two markets scraped in one run may be priced over different numbers of nights. Every record carries `nights` and `datesWereRequested` so you can tell. Pin `checkIn` and `checkOut` and the problem disappears. Half a range is no better than none — Airbnb ignores a lone check-in, so the Actor drops it rather than label its own guess with your date.

**Currency follows the IP unless you pin it.** An Austin search run from a European exit came back in euros. The Actor sends `currency` on every request for that reason, and `locale=en` with it so that "1.234" cannot mean one thousand on one row and one-point-two on the next.

**Airbnb re-ranks between requests, so pages overlap.** Consecutive pages repeated 2 of 18 results on one hop, and a three-market run turned up **29 duplicates in 245 fetched rows — 12%**. They are removed by listing id, are not charged, and are counted in the log. Left in, they would inflate the count and pull the median toward whatever the ranker currently favours.

**Airbnb does not always send the same payload, so the breakdown checks itself.** Most responses carry a full price explanation. Some carry a reduced one — no tax line, no `Total` line, and on one platform run a positive line equal to the whole subtotal, which naively read would have reported a $2,291 discount on a $2,291 stay. Every record is therefore checked against its own total: if `subtotal − discount + fees + taxes` does not reach `totalPrice`, the components are set to null and `priceBreakdownComplete` is false. `nightlyRate`, `accommodationTotal` and `totalPrice` are read directly and always stand. The same reduced payload omits the card title, which is where `propertyType` and `listingCity` come from — the summary reports that as `typeUnknown` rather than letting the type counts quietly fall short. A residential proxy returns the full payload more often than a datacenter one.

**Search stops at 15 pages.** That is 270 stays per search, and it is Airbnb's limit, not a setting. Markets that hit it are flagged `truncated: true`. The way past it is to split the market into price bands, or to search neighbourhoods rather than a whole city.

***

### Output

```json
{
  "recordType": "listing",
  "listingId": "1239199837879387949",
  "name": "Studio Haus | Short Drive to ACL + Walk to Brewery",
  "propertyType": "Apartment", "listingCity": "Austin",
  "latitude": 30.262, "longitude": -97.7229,
  "nightlyRate": 284.2, "nights": 5,
  "accommodationTotal": 1421, "taxes": 241.57, "totalPrice": 1662.57,
  "currency": "USD", "datesWereRequested": true,
  "rating": 4.68, "reviewsCount": 189, "isGuestFavorite": true,
  "bedText": "1 bedroom", "bathText": "1 bath", "bathrooms": 1
}
```

Three ready-made views: **Stays**, **Price breakdown** and **Market summary**. Set `flattenOutput: true` for CSV.

***

### Honest limits

- **`bedrooms` and `beds` are never inferred from each other.** Airbnb publishes whichever it has — "1 queen bed" for one listing, "2 bedrooms" for the next — and converting one into the other would invent a number the host never stated. The raw `bedText` is kept alongside.
- **A city search reaches past the city.** An Austin search returns Pflugerville and Cedar Park; a Paris one returns Aubervilliers and Saint-Ouen. `listingCity` says which, so you can filter.
- **Prices are what Airbnb quotes for your dates and party size**, not a historical average and not an occupancy figure.
- **No amenities, host details or review text.** Those live on the individual room page, which costs several hundred kilobytes each; this Actor stays on search results, which is what makes it cheap.
- **`totalPrice` comes from Airbnb's exact Total line** where one exists, falling back to the rounded headline where it does not.
- **`propertyType` and `listingCity` come from Airbnb's card title**, which is absent on some responses. Null rather than parsed out of a host's marketing headline, and counted as `typeUnknown` on the summary.
- **Blocked markets, empty markets and duplicate rows are never charged.**

***

### Pricing

| Event | Price | When |
|---|---|---|
| Actor start | $0.005 | Once per run |
| Market scraped | $0.006 | Per market paged and summarised |
| Stay scraped | $0.0008 | Per stay returned, after filters |

**$0.80 per 1,000 stays.** A 20-market weekly tracker at 180 stays each costs about **$3.00 a run**. AirDNA and Mashvisor start at $20–100 a month per market.

***

### Tips

- **Scrape the same dates weekly and diff on `listingId`.** Rising median rate with steady supply is a market tightening; rising supply with a flat rate is the opposite.
- **Watch `newListings`.** Unreviewed stays are supply that has just arrived — the leading indicator of a market getting more competitive.
- **Compare `nightlyRate` against `totalPrice / nights`.** The gap is the fee load, and a market with a high one is a market where guests are quietly paying more than the headline.
- **Run one search per price band** to get past the 270 ceiling and see how each tier moves separately.
- **Pair with a for-sale price** to turn a median nightly rate into a yield: 180 nights x median rate, against the purchase price.

# Actor input Schema

## `locations` (type: `array`):

One per line. A plain place name works — "Austin, TX", "Paris", "Lisbon, Portugal" — or paste an airbnb.com/s/... search URL if you have already filtered it on the site.

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

Alternative to Places, for feeding output from another Actor straight into this one.

## `sourceDatasetId` (type: `string`):

Read places from an existing dataset instead of typing them.

## `sourceDatasetField` (type: `string`):

Which field on the source dataset holds the place or search URL.

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

Strongly recommended. Without dates Airbnb picks its own window and applies its own long-stay discounts, so prices are not comparable between markets or between runs.

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

Must be after check-in. Ignored unless check-in is also set.

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

Guest count changes which listings qualify and what they cost. 0 leaves it to Airbnb.

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

Ages 2-12. 0 disables.

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

Under 2. 0 disables.

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

0 disables. Filters to listings that allow them.

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

ISO code such as USD, EUR or GBP. Always sent, because Airbnb otherwise picks a currency from the proxy's IP address — the same market priced from two countries returns two incomparable answers.

## `roomTypes` (type: `array`):

Leave empty for all. Valid values: entire-home, private-room, shared-room, hotel.

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

In the currency above. 0 disables.

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

In the currency above. 0 disables. Pairing a min and max is also how to get past the search-depth ceiling: run the same market in price bands.

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

0 disables.

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

0 disables. Beds and bedrooms are different claims and Airbnb lists whichever it has.

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

0 disables.

## `superhostOnly` (type: `boolean`):

Restrict to Superhost listings.

## `maxListingsPerLocation` (type: `integer`):

Airbnb's search returns 18 per page and stops handing out cursors after 15 pages, so 270 is the hard ceiling per search. Narrow with dates, price bands or room type to see past it.

## `includeMarketSummary` (type: `boolean`):

Add one rollup record per market with median nightly rate, median total, rating and the whole-home versus private-room split. Not billed as a stay.

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

Markets processed in parallel. Airbnb answers bursts with a page that has no search data on it, so this is deliberately conservative.

## `requestTimeoutSecs` (type: `integer`):

Per-request timeout.

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

Retries for connection resets and 5xx responses. Blocked pages are retried separately with a backoff.

## `flattenOutput` (type: `boolean`):

Emit flat dot-notation columns. The stay records are already flat, so this mainly affects badges and fee labels.

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

Recommended. Airbnb rate-limits by address, and the proxy country also decides which currency it would default to if one were not pinned.

## Actor input object example

```json
{
  "locations": [
    "Austin, TX"
  ],
  "sourceDatasetField": "searchUrl",
  "adults": 2,
  "children": 0,
  "infants": 0,
  "pets": 0,
  "currency": "USD",
  "minPrice": 0,
  "maxPrice": 0,
  "minBedrooms": 0,
  "minBeds": 0,
  "minBathrooms": 0,
  "superhostOnly": false,
  "maxListingsPerLocation": 180,
  "includeMarketSummary": true,
  "maxConcurrency": 3,
  "requestTimeoutSecs": 60,
  "maxRetries": 2,
  "flattenOutput": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

Stay records carry recordType 'listing'; rollups carry 'market-summary'.

# 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 = {
    "locations": [
        "Austin, TX"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("eiv/airbnb-market-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 = { "locations": ["Austin, TX"] }

# Run the Actor and wait for it to finish
run = client.actor("eiv/airbnb-market-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 '{
  "locations": [
    "Austin, TX"
  ]
}' |
apify call eiv/airbnb-market-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,eiv/airbnb-market-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/JdQATeBxxisL8z6se/builds/CxncnJRrJQaOuTN14/openapi.json
