# Airbnb Scraper — Listings, Prices & Photos (`danielmachad/airbnb-market-intel`) Actor

Scrape Airbnb listings: prices, photos, ratings, coordinates, room details. Price split into list rate, discounts and taxes so it reconciles. Market percentiles and price changes. No host data.

- **URL**: https://apify.com/danielmachad/airbnb-market-intel.md
- **Developed by:** [Daniel](https://apify.com/danielmachad) (community)
- **Categories:** Travel, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.25 / 1,000 listings

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/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 Scraper — Listings, Prices & Photos

Scrape Airbnb search results for any location: nightly prices, photos, ratings, review counts, coordinates, room details and availability. Fast, priced per listing, and no scraping infrastructure to run.

Where it differs from other Airbnb scrapers is the price. Airbnb's headline figure hides what is inside it, and most exports pass that figure straight through. This one takes the stay apart.

### What you get

**Listing data** — name, property type, neighborhood, bedrooms, beds, baths, coordinates, rating, review count, Guest Favorite badge, photo URLs, direct link.

**Prices that reconcile** — every row breaks the stay into list rate, discounts, taxes and final total, so `base − discounts + taxes = total` holds on every line. This matters more than it sounds. A Barcelona listing in our own test data advertises **€221.97/night** and charges an effective **€148.36** after a long-stay discount — a third less. An export carrying one "price" field is quietly telling you one of those two numbers and not saying which. You get `pricePerNight` (what the host advertises) and `effectiveNightlyRate` (what a guest pays) as separate fields, plus a `totalIncludesTaxes` flag because some markets, Amsterdam among them, fold tourist tax into the total without itemising it.

**Multi-unit inventory** — professional operators running several units of one property carry an inventory line instead of a room count. Most exports drop it, or worse, mis-read "3 apartments available" as a bedroom count. Here it becomes `unitsAvailable` plus the raw wording, which also carries scarcity ("1 apartment left").

It separates markets sharply. In our own test runs, the first 50 results for **Barcelona returned 23 multi-unit properties** — 22 `Rental unit` and one `Serviced apartment`. The identical query against **Porto, Austin and Mexico City returned none at all**: those results were private apartments, condos and lofts. If you benchmark a listing against "comparable" ones without that distinction, you may be pricing an owner's spare flat against a professional operator's building.

**Market position** — where each listing's rate sits in the market (percentile) and how far it is from the median.

**What changed since your last run** — price moves in currency and percent, new reviews, and a `new` / `changed` / `unchanged` status. Delisted properties are counted in the summary.

**Occupancy and revenue estimates** — reviews per month, estimated occupancy, estimated monthly revenue.

**Market summary** — median, 25th and 75th percentile rates, min/max, average rating, Guest Favorite share, new and removed listing counts, median price movement. Delivered nested on every row *and* under the `MARKET_SUMMARY` key, so a single dataset call is enough for no-code integrations.

### Also included, at no extra charge

**Market position** — each listing's price percentile and distance from the market median, plus a market summary with median, quartiles, average rating and Guest Favorite share. You pay for listings; the aggregates come with them.

**Run-to-run changes** — run the same search again and every row reports what moved: price change in currency and percent, new reviews, and a `new` / `changed` / `unchanged` status. Delisted properties are counted in the summary. Nothing to configure; it keys off the search you already ran.

**Occupancy estimates** — derived from review growth between runs, so they appear from the second run onwards. See the section below for the method and its assumptions.

### Who uses this

- **Developers and data teams** — a clean, per-listing feed to load straight into your own pipeline.
- **Short-term rental hosts and managers** — see where your rate sits against comparable listings, and catch competitors repricing.
- **Investors and analysts** — measure ADR, price dispersion and booking pace before committing to a market.
- **Researchers** — reproducible market snapshots with the aggregate math already done.

### Sample output

```json
{
  "listingId": "652910731756745793",
  "name": "Chic 1-BR by Rua Augusta Arch",
  "roomType": "Condo",
  "neighborhood": "Santo António",
  "bedrooms": 1,
  "beds": 4,
  "baths": 1.5,
  "lat": 38.70767,
  "lng": -9.13672,
  "rating": 4.92,
  "reviewCount": 198,
  "pricePerNight": 92.89,
  "effectiveNightlyRate": 89.04,
  "basePrice": 278.66,
  "discountTotal": 11.53,
  "taxes": 56.09,
  "totalPrice": 323.22,
  "nights": 3,
  "currency": "USD",
  "totalIncludesTaxes": true,
  "isGuestFavorite": true,
  "unitsAvailable": null,
  "availabilityNote": null,
  "photoCount": 5,
  "photos": [
    "https://a0.muscache.com/im/pictures/miso/Hosting-652910731756745793/original/3cc7538b.jpeg",
    "https://a0.muscache.com/im/pictures/miso/Hosting-652910731756745793/original/31311278.jpeg"
  ],
  "url": "https://www.airbnb.com/rooms/652910731756745793",

  "pricePercentile": 87.5,
  "priceVsMarketMedianPct": 39.99,

  "status": "changed",
  "priceChange": 10,
  "priceChangePct": 6.5,
  "reviewCountChange": 2,
  "daysSincePreviousRun": 7,

  "reviewsPerMonth": 8.57,
  "estimatedOccupancy": 46.7,
  "estimatedMonthlyRevenue": 2288.26,
  "occupancyBasis": "review_delta",

  "market": {
    "listingCount": 36,
    "priceMedian": 122.9,
    "priceP25": 116.03,
    "priceP75": 161.34,
    "ratingAvg": 4.79,
    "guestFavoriteShare": 38.89,
    "newListings": 10,
    "removedListings": 10,
    "medianPriceChangePct": 0,
    "avgEstimatedOccupancy": 61.2
  },
  "scrapedAt": "2026-08-10T03:59:30.643Z"
}
```

### Input

| Field | Default | Notes |
|---|---|---|
| `location` | `Lisbon, Portugal` | Use a `City, Country` form. |
| `checkIn` / `checkOut` | none | **Pass these.** See the note below. |
| `adults` | `2` | Guest count used for pricing. |
| `maxListings` | `50` | Caps both results and cost. Maximum 270. |
| `currency` | `USD` | All prices come back in this currency. |
| `trackChanges` | `true` | Stores a snapshot so the next run can report changes. |
| `reviewRate` | `0.5` | Occupancy model — share of stays leaving a review. |
| `avgStayNights` | `3.5` | Occupancy model — average nights per booking. |

The defaults run as-is and return a usable Lisbon market scan without editing anything.

**Always pass check-in and check-out dates.** Without them Airbnb picks a *different* date window for each listing — one priced for a 5-night August stay, the next for a 5-night September stay. The rates then are not comparable to one another, and the percentiles built from them are meaningless. The Actor warns you when dates are missing.

### Pricing

Pay per event, **$1.25 per 1,000 listings**. A 50-listing scan costs $0.06.

Photos, the price breakdown, market percentiles, change tracking and occupancy estimates are all included. You pay per listing, and for nothing else.

### How the occupancy estimate works

Airbnb does not publish occupancy, so every tool in this space infers it. This one measures **review growth between your runs**:

```
bookings      = new reviews ÷ reviewRate
booked nights = bookings × avgStayNights
occupancy     = booked nights ÷ days elapsed
```

Both assumptions are inputs, not hidden constants — if you have better figures for your market, set them and the numbers move with you.

What this means in practice:

- **The first run cannot report occupancy.** There is no history to measure against yet. Those fields come back `null` with `occupancyBasis: "insufficient_history"`.
- **Runs less than 3 days apart are rejected for occupancy.** Over a short window a single review would imply a fully booked month. The listing data still comes back normally.
- **Weekly runs give the cleanest signal.** Review pace is noisy day to day and settles over a week or more.

Deliberate design choice: an occupancy number derived from a lifetime review count and a guessed listing age would be available on run one, and would be fiction. This returns nothing until it can measure something real.

### Change tracking

Snapshots are keyed by market slice — location, dates, guest count and currency together. Run the same configuration again and you get the deltas. Change any of those and you are tracking a different market, so the comparison restarts.

This is what makes the Actor worth scheduling: set it weekly and you accumulate a price and booking-pace history for your market instead of one disconnected export.

### Use it from an AI agent (MCP)

This Actor works as a tool for AI agents through the [Apify MCP server](https://github.com/apify/apify-mcp-server) — no extra setup on your side. An agent connected to it can find this Actor with `search-actors`, read its parameters with `fetch-actor-details`, and run it with `call-actor`.

The input schema is written to be read by a model, not just by a person: date formats are stated explicitly, cost-bearing parameters say that they bear cost, and every field that stays empty on a first run says so. An agent should get a usable call right the first time.

A minimal call needs nothing but a location:

```json
{ "location": "Lisbon, Portugal" }
```

A precise one pins the dates so the rates are comparable:

```json
{
  "location": "Lisbon, Portugal",
  "checkIn": "2026-09-15",
  "checkOut": "2026-09-18",
  "maxListings": 50,
  "currency": "EUR"
}
```

Bad input fails fast with a message that says how to fix it, rather than burning a run on a meaningless window.

### Limits, stated plainly

- **270 listings per market slice, maximum.** That is Airbnb's own pagination ceiling for a search, not a limitation added here. For a large city, that is the top-ranked slice, not every property in town. To go deeper, run several narrower searches — by neighborhood, by date window, or by guest count.
- **Adjacent result pages overlap.** The Actor fetches a spare page to absorb it, but very small or very specific markets may still return fewer listings than requested.
- **Percentiles are built on the list rate**, not the effective one, because that is the number a host sets and competitors compare against. `effectiveNightlyRate` is there if you would rather rank by what guests pay.
- **`neighborhood` is whatever Airbnb puts in the headline**, which for some listings is the city rather than a district. Check `headlineCoverage` in the market summary to see how much of the run carried a headline at all.
- **Occupancy is an estimate**, not reported data. It is only as good as the two assumptions above.
- **No availability calendar.** This reads search results, so a listing missing from results is not proof it is booked — it may simply rank lower.

### Data and privacy

This Actor returns **no personal data**. Host names, host IDs, profile photos and profile links are dropped before anything is written, deliberately and by design. What comes back is commercial fact about an accommodation: type, size, location, price, rating, review count.

It reads only publicly visible search results, logged out. It does not sign in, does not bypass any technical restriction, and does not republish listing photos or description text — only extracted facts.

### FAQ

**Does it work for any city?**
Any market Airbnb recognises. Use a `City, Country` form. If a location returns nothing, the run fails with a clear message rather than charging you for an empty result.

**Why is `bedrooms` sometimes empty?**
Shared rooms and hotel rooms are listed by Airbnb without a bedroom count. `beds` is still populated.

**Can I run this on a schedule?**
Yes, and it is built for it. Use Apify Schedules with the same input and each run reports what changed since the last.

**Do I need a proxy?**
No. Proxy is optional and set to automatic by default. If your proxy is unavailable the run continues rather than failing.

**Can I get the data into Sheets, Make or Zapier?**
Yes. Every row is the same shape and carries the market aggregates nested under `market`, so one dataset read gives you both the listings and the market context.

# Actor input Schema

## `location` (type: `string`):

Market to analyse, as "City, Country" — for example "Lisbon, Portugal", "Austin, Texas" or "Kyoto, Japan". A bare city name usually resolves too, but the two-part form is unambiguous. The run fails with an explanatory message if Airbnb does not recognise the place, rather than returning an empty result.

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

Date in YYYY-MM-DD format, e.g. 2026-09-15. Must be in the future. Supply this together with checkOut: without dates, Airbnb prices each listing for a different date window, so the nightly rates are not comparable to one another and the market percentiles built from them are meaningless.

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

Date in YYYY-MM-DD format, e.g. 2026-09-18. Must be after checkIn. The gap between the two dates sets the stay length used for pricing.

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

Guest count used for the price query.

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

Upper bound on listings returned, and therefore on cost — billing is per listing. Airbnb serves at most 270 per search, so that is the ceiling; a narrow market may return fewer than requested. To cover a large city beyond 270, run several searches split by neighborhood or date window.

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

Currency for all returned prices.

## `trackChanges` (type: `boolean`):

Stores a snapshot of this market slice so a later run can report what moved. The change fields (priceChange, reviewCountChange, status) and every occupancy field stay null on the first run for a given slice — there is no history to compare against yet. A slice is the combination of location, dates, adults and currency: repeat the exact same input to build history, change any of it and comparison restarts. Occupancy additionally requires the runs to be at least 3 days apart.

## `reviewRate` (type: `number`):

Assumed share of stays that leave a review. Drives the occupancy estimate. Lower values imply higher occupancy for the same review count.

## `avgStayNights` (type: `number`):

Assumed average length of stay, used to convert bookings into booked nights.

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

Optional. Leave as is — the automatic setting is what this Actor is tuned for. Residential proxy is deliberately ignored: Airbnb does not block Apify's shared addresses, so it adds cost without improving results. Supply proxyUrls if you need to route through your own infrastructure.

## Actor input object example

```json
{
  "location": "Lisbon, Portugal",
  "adults": 2,
  "maxListings": 50,
  "currency": "USD",
  "trackChanges": true,
  "reviewRate": 0.5,
  "avgStayNights": 3.5,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# 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 = {
    "location": "Lisbon, Portugal"
};

// Run the Actor and wait for it to finish
const run = await client.actor("danielmachad/airbnb-market-intel").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 = { "location": "Lisbon, Portugal" }

# Run the Actor and wait for it to finish
run = client.actor("danielmachad/airbnb-market-intel").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 '{
  "location": "Lisbon, Portugal"
}' |
apify call danielmachad/airbnb-market-intel --silent --output-dataset

```

## MCP server setup

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

```

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/fIH3KbUu2RgQUT3As/builds/DmbHO5zShTCSDLcLY/openapi.json
