# Airbnb Scraper: Listings, Prices & Availability by Location (`changefeeds/airbnb-listings-scraper`) Actor

Search Airbnb by city or pasted search URL and export listings with prices, ratings, rooms and coordinates. Add dates for exact stay prices and availability, or pass listing URLs for full details: host, amenities, rules. Public logged-out data, no login.

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

## Pricing

Pay per event

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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Airbnb Scraper: Listings, Prices & Availability by Location

By Changefeeds Tools. An Airbnb scraper that searches by location, prices by
date and reports availability. Search a city or paste an airbnb.com search
URL to export listings with prices, ratings, room counts, coordinates and
badges. Add check-in and check-out dates for real stay prices, or pass
specific listing URLs for full details: description, amenities, house rules,
host and sub-ratings, plus price and availability for your dates. It reads
the same public pages a logged-out browser on airbnb.com gets. No Airbnb
account, cookie, or login.

Who it is for: short-term-rental analysts comparing a market, hosts watching
competitors' prices, travel and relocation tools, and anyone who needs a
spreadsheet of what Airbnb shows for a place and a date range.

### Input

| Field | Type | Default | What it does |
| --- | --- | --- | --- |
| `locations` | list of strings | | A place name per line (`"Lisbon, Portugal"`), or a full `airbnb.com/s/...` search URL copied from your browser. A pasted URL is used as-is, with all its filters (room type, amenities, map area). |
| `listingUrls` | list of strings | | `airbnb.com/rooms/<id>` URLs (any Airbnb country domain) or bare numeric ids. One detail row each. |
| `checkIn` / `checkOut` | `YYYY-MM-DD` | none | Optional; give both or neither. With dates, search prices are for your stay and listing URLs also get a price and availability. |
| `adults` | integer 1-16 | `2` | Guests. Changes which listings show and their prices. |
| `currency` | 3-letter code | `USD` | `EUR`, `GBP`, `JPY`, ... A pasted search URL that already has `currency=` keeps its own. |
| `minPrice` / `maxPrice` | integer | none | Airbnb's own per-night price filter, in `currency`. Searches only. |
| `maxItems` | integer 1-270 | `100` | Max listings per location. |
| `includeDetails` | boolean | `false` | Also open each search result's listing page and add the detail fields (one extra page per listing; see Pricing). |
| `proxy` | proxy settings | Apify Proxy, residential | Airbnb blocks many datacenter IPs, so requests go through residential proxy by default. |

Search a city, cheapest way:

```json
{ "locations": ["Lisbon, Portugal"], "maxItems": 20 }
```

Priced for your dates, with details:

```json
{ "locations": ["Porto, Portugal"], "checkIn": "2026-11-20", "checkOut": "2026-11-23", "currency": "EUR", "minPrice": 60, "maxPrice": 150, "includeDetails": true, "maxItems": 50 }
```

Specific listings, with availability:

```json
{ "listingUrls": ["https://www.airbnb.com/rooms/12537150", "1144816383633864634"], "checkIn": "2026-11-28", "checkOut": "2026-12-03" }
```

### Sample output

A search row (live run, 2026-09-30, no dates given; `images` shortened):

```json
{
  "type": "listing",
  "id": "12537150",
  "url": "https://www.airbnb.com/rooms/12537150?check_in=2026-11-28&check_out=2026-12-03&adults=2",
  "name": "Deluxe apartment in Chiado",
  "title": "Apartment in Santa Maria Maior",
  "subtitle": "Álvaro Siza Vieira building in Chiado, with a modern interior and A/C",
  "roomType": null,
  "propertyType": "Apartment",
  "area": "Santa Maria Maior",
  "city": null,
  "lat": 38.7118,
  "lng": -9.13868,
  "rating": 4.95,
  "reviewsCount": 625,
  "isNew": false,
  "isSuperhost": null,
  "guestFavorite": true,
  "price": {
    "amount": 795,
    "currency": "USD",
    "qualifier": "total",
    "nights": 5,
    "originalAmount": null,
    "label": "$795 for 5 nights",
    "checkIn": "2026-11-28",
    "checkOut": "2026-12-03",
    "datesChosenByAirbnb": true,
    "breakdown": [{ "description": "5 nights x $158.95", "amount": 794.73, "text": "$794.73" }],
    "notes": []
  },
  "available": null,
  "unavailableReason": null,
  "priceError": null,
  "images": ["https://a0.muscache.com/im/pictures/airflow/Hosting-12537150/original/2121117c-5284-43d2-8bdb-dc3723827676.jpg"],
  "badges": ["Guest favorite"],
  "paymentMessages": ["Free cancellation"],
  "bedrooms": 1,
  "beds": 2,
  "bathrooms": 1,
  "guests": null,
  "propertyId": null,
  "searchInput": "Lisbon, Portugal",
  "position": 1,
  "hasDetails": false,
  "detailsError": null,
  "description": null,
  "amenities": null,
  "houseRules": null,
  "highlights": null,
  "host": null,
  "subRatings": null,
  "scrapedAt": "2026-09-30T05:37:35.346Z"
}
```

With `includeDetails` on, or for any `listingUrls` row, the same row also
carries the listing page's fields (live run, shortened):

```json
{
  "roomType": "Entire home/apt",
  "propertyType": "Entire rental unit",
  "city": "Porto",
  "isSuperhost": true,
  "guests": 2,
  "hasDetails": true,
  "description": "Beautiful studio apartment set in a fully renovated 18th-century building, with air conditioning for heating and cooling…",
  "amenities": ["River view", "Garden view", "Hair dryer", "Shampoo", "…38 in total"],
  "houseRules": ["Check-in: 3:00 PM - 12:00 AM", "Checkout before 11:00 AM", "Self check-in with keypad", "2 guests maximum", "No pets"],
  "highlights": ["Top 10% of homes", "Self check-in", "Vibrant neighborhood"],
  "host": { "id": "245989121", "name": "Isabel & Igor", "isSuperhost": true, "isVerified": true, "rating": 4.9, "reviewsCount": 4616, "yearsHosting": 7, "responseRate": "Response rate: 100%", "responseTime": "Responds within an hour" },
  "subRatings": { "accuracy": 4.93, "checkIn": 4.97, "cleanliness": 4.91, "communication": 4.98, "location": 4.95, "value": 4.89 }
}
```

A listing URL with dates it is booked for comes back as
`"available": false, "unavailableReason": "Those dates are not available", "price": null`.
With dates it is free for, `price` holds the booking-box total, e.g.
`"$795 for 5 nights"`, with `"available": true`.

A failed input gets a free `status` row instead:

```json
{ "type": "status", "input": "https://www.airbnb.com/rooms/1", "source": "listingUrls", "status": "not_found", "error": "Listing 1 not found (removed, unlisted, or never existed).", "checkedAt": "2026-09-30T05:37:53.680Z" }
```

`status` is one of:

- `not_found`: the listing is gone, or Airbnb did not recognise the place name (it fell back to a generic map area).
- `invalid`: the entry is not a place, search URL, listing URL or id (a `/rooms/` URL put in `locations` is pointed at `listingUrls`), or a duplicate listing.
- `blocked`: Airbnb's bot protection refused the request from every proxy IP tried.
- `error`: any other HTTP or network failure.

The key-value store record `OUTPUT` holds a run summary: counts by type,
`charged_events`, `stopped_reason`, `truncated_inputs`, and per location the
search URL used, Airbnb's own heading ("Over 1,000 homes in Lisbon"), pages
fetched and why it stopped. Every run leaves at least one dataset row, so a
quiet run is never mistaken for a broken one.

### Pricing

Pay per event, nothing else:

- **`listing-returned`: $0.003 per listing row** ($3 per 1,000 listings).
- **`details-added`: $0.002 extra per listing with details** (search results
  with `includeDetails` on, and every `listingUrls` row, since those always
  come from the listing page). A search row whose listing page could not be
  read is delivered with `detailsError` and charged as a plain listing.

So a plain search costs $3 per 1,000 listings, and a search with details or a
list of listing URLs costs $5 per 1,000. Proxy traffic is included in the
price. Failed inputs (`not_found`, `invalid`, `blocked`, `error`) are never
charged. If you set a maximum total charge for the run, the actor checks the
remaining budget before fetching each page, stops cleanly once it is
reached, and says so in `OUTPUT.stopped_reason`
(`"max_total_charge_reached"`).

### Limits, stated plainly

- **Airbnb shows at most 15 pages of 18 results per search, and the pages
  overlap.** A live 15-page run for "Lisbon, Portugal" returned 242 unique
  listings, although Airbnb says "Over 1,000 homes". When a search hits that
  wall the actor marks it `truncated` and lists it in
  `OUTPUT.truncated_inputs`. To get more, split the search: run the same place
  in price bands (`minPrice`/`maxPrice`, e.g. 0-80, 80-150, 150-300, 300+), by
  neighbourhood, or paste smaller map-area search URLs. `maxItems` is capped
  at 270 for this reason.
- **Without dates, the prices are for dates Airbnb picked.** Airbnb then
  prices each result for its own sample stay (often 5 nights, different per
  listing). The row says so: `price.datesChosenByAirbnb: true`, with those
  dates in `price.checkIn` / `price.checkOut`. Give dates for comparable
  prices.
- **Prices are what the card or booking box shows**, in the display format
  Airbnb chooses for your currency and region. Depending on the region that
  may be before taxes. `price.breakdown` and `price.notes` carry Airbnb's own
  lines. Hotels can show "Price may vary by room type".
- **`isSuperhost` is `null` on plain search rows** unless the card shows a
  Superhost badge; search cards do not say when a host is not one. The
  listing page (`includeDetails`) gives the real value.
- **Listing URLs get a price only with dates.** The listing page has no price
  on its own. With dates, the actor makes one more request for the listing's
  booking box. If that call fails, the row is still delivered with
  `priceError`.
- **Search results include what Airbnb puts in them**, such as "Featured
  hotel" placements (`badges`, `propertyId` set).
- **Not supported:** vanity `/h/...` links (use the `/rooms/<id>` URL),
  experiences, reviews text, calendars beyond the dates you give, and
  anything that needs a login. The actor never solves CAPTCHAs and never
  uses an account. If Airbnb challenges a request, it retries from a new
  proxy IP (up to 8 times), then reports a free `blocked` row.
- **It stays polite:** search pages one at a time, at most 2 listing pages in
  flight, at least 500 ms between requests, 20 s timeouts, and HTTP 429/5xx
  retried with backoff (up to 3 retries, Retry-After honoured and capped at
  60 s).
- **Airbnb changes its site.** The actor reads the data embedded in Airbnb's
  own pages, and it finds the booking-box query id in Airbnb's current
  JavaScript at run time rather than hard-coding it. A layout change can
  still break a field until the actor is updated; a page without the
  expected data becomes an `error` row, never a silently empty result.

### FAQ

**Do I need an Airbnb account?** No. Only public, logged-out pages.

**Can I use my own search with filters?** Yes. Set up the search on
airbnb.com (any country domain), copy the URL from the address bar and put
it in `locations`. Its filters are kept. Your `checkIn`, `checkOut`,
`adults`, `currency` and price fields are only added where the URL does not
already set them.

**Why fewer listings than "Over 1,000 homes"?** See the 15-page limit above:
split the search into price bands.

**Why is `price` null?** On a listing URL without dates, Airbnb shows no
price. On one with dates, the listing may be unavailable for them (see
`available` and `unavailableReason`), or the booking-box call failed
(`priceError`).

### Local development

```bash
pnpm --filter @mmnm/airbnb test        # unit tests on saved Airbnb responses, no network
pnpm --filter @mmnm/airbnb build
```

`node src/main.ts` runs the actor locally with Apify's local storage
(`./storage`, input in `storage/key_value_stores/default/INPUT.json`);
leave `proxy` out of the input to fetch directly from your own connection.

# Actor input Schema

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

Places to search, one per line: a city, region or neighbourhood ("Lisbon, Portugal"), or a full airbnb.com/s/... search URL copied from your browser, which is used as-is with all its filters (room type, amenities, map area). Each entry returns up to `maxItems` listings.

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

Specific listings, one per line: airbnb.com/rooms/<id> URLs (any Airbnb country domain) or bare numeric ids. Each returns one row with full details. With check-in/check-out dates, the row also has the price and availability for those dates.

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

YYYY-MM-DD. Optional; give it together with check-out. Without dates, Airbnb prices each search result for sample dates it picks itself (reported in price.checkIn / price.checkOut), and listing URLs come back without a price.

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

YYYY-MM-DD, after check-in.

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

Number of adult guests, 1 to 16. Affects which listings are shown and their prices.

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

3-letter currency code for prices, e.g. USD, EUR, GBP, JPY. A pasted search URL that already has a currency keeps its own.

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

Optional. Airbnb's own price filter, per night, in the chosen currency. Searches only. Splitting one search into price bands is the way to get past Airbnb's 15-page limit (see README).

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

Optional. Airbnb's own price filter, per night, in the chosen currency. Searches only.

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

Max listings returned per location, 1 to 270. Airbnb itself shows at most 15 pages of 18 results per search, and pages overlap, so a single search usually tops out around 240 unique listings.

## `includeDetails` (type: `boolean`):

Also open each search result's listing page and add the description, amenities, house rules, host, room type and sub-ratings. One extra page per listing, so slower, and charged as details-added per listing (see Pricing).

## `proxy` (type: `object`):

Airbnb blocks many datacenter IPs, so requests go through Apify Proxy by default (residential). Proxy traffic is included in the price. Turn it off only if you run the Actor from your own network.

## Actor input object example

```json
{
  "locations": [
    "Lisbon, Portugal"
  ],
  "adults": 2,
  "currency": "USD",
  "maxItems": 20,
  "includeDetails": false,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

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

No description

## `summary` (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 = {
    "locations": [
        "Lisbon, Portugal"
    ],
    "maxItems": 20,
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("changefeeds/airbnb-listings-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": ["Lisbon, Portugal"],
    "maxItems": 20,
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("changefeeds/airbnb-listings-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": [
    "Lisbon, Portugal"
  ],
  "maxItems": 20,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call changefeeds/airbnb-listings-scraper --silent --output-dataset

```

## MCP server setup

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