# Airbnb Listings Scraper - Prices, Ratings & Geo (`scrapyx/airbnb-listings-scraper`) Actor

Scrapes Airbnb stay listings from the search page's own embedded data: price, rating, coordinates, photos and room type. Deduplicates the listings Airbnb repeats within a page, decodes the real listing id, and reports the currency the page actually rendered.

- **URL**: https://apify.com/scrapyx/airbnb-listings-scraper.md
- **Developed by:** [Ibnu Adzim](https://apify.com/scrapyx) (community)
- **Categories:** Travel, Business, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.26 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 Listings Scraper — Prices, Ratings & Geo

Reads Airbnb stay listings from the search page's **own embedded data** — the
`data-deferred-state` blob the site server-renders. No API key, no GraphQL
call, no account.

### What you get

| `recordType` | One per | Carries |
| --- | --- | --- |
| `LISTING` | stay | real listing id and `/rooms/` URL, name, room type and area, latitude/longitude, rating (numeric **and** as Airbnb labelled it), badges, photos, and the price with the window it actually covers |
| `SEARCH_SUMMARY` | destination | listings returned vs **raw result objects**, duplicates removed within and across pages, the currency requested vs the one rendered, and the stay lengths the prices cover |
| `ERROR` | failed input | a named reason |

### Four things the payload will mislead you about

**1. One page reports 48 results that are 30 listings.** The deferred state
carries the set across overlapping sections — 18 ids appear exactly twice, same
title, same price, nothing malformed. Counting result objects overstates by
**60%**. This actor deduplicates within a page as well as across pages, and
reports `rawResultObjects` beside `listingsReturned` so the gap stays visible.

**2. The currency follows the exit IP.** Same listing, same session, seconds
apart:

```
(no currency parameter)  ->  Rp 17,326,505     <- Rupiah, from the address
currency=USD             ->  $3,246
currency=EUR             ->  EUR 2,785
```

Nothing asked for Rupiah. So a local run and a cloud run through a US proxy
silently produce different numbers for the same listing. An unknown code isn't
refused either — it's *sticky*, keeping whatever was in effect. This actor
always sends the currency explicitly, validates it, and reports
`currenciesReturned` read from the rendered price symbol so a mismatch is
visible rather than assumed away.

**3. The headline price is a stay total, not a nightly rate.** With no dates,
Airbnb invents its own window:

```
no dates                    "$3,246"  qualifier "for 5 nights"
checkin/checkout supplied   "$795"    qualifier "for 3 nights"
```

Reading that as a per-night price is wrong by whatever the window happened to
be — and the window changes with the search. Every row carries
`priceQualifier`, `priceNights` and `priceIsStayTotal`, and
`pricePerNightApprox` is derived **only** when the night count is actually
known.

**4. The listing id is base64 and isn't the one in the URL.**
`RGVtYW5kU3RheUxpc3Rpbmc6MTY3NTM4…` decodes to
`DemandStayListing:1675389828808490095`. The opaque form joins to nothing —
including Airbnb's own `/rooms/<id>` URLs. Decoded here, with the raw form kept
beside it.

### Smaller things

- **Rating is a localized string.** A listing with no reviews comes back as the
  word **"New"**, not `0` or `null`. Both survive: `ratingValue` (numeric, null
  for New), `ratingLabel` (Airbnb's own text) and an `isNewListing` flag — so a
  new listing never gets averaged in as a zero.
- **Pagination cursors are constructed, not scraped.** They're base64 of
  `{"section_offset":0,"items_offset":N,"version":1}` stepping by 18, and the
  server accepts ones this actor builds — so there's no cursor bookkeeping and
  no dependence on the page exposing them.
- `title` is the room type and area ("Apartment in Paris"); the human name of
  the place is a separate field.

### Notes

- `airbnb.com/robots.txt` scopes its Claude rules to specific paths and leaves
  `/s/` and `/rooms/` allowed.
- **DataDome runs on this origin.** It did not challenge during development,
  but a datacenter exit is far more likely to be challenged than a residential
  one. A challenge is reported as a named `challenged` error rather than
  silently returning nothing.
- The proxy setting changes the **data** here, not just the routing — see the
  currency point above. Residential is recommended for anything large.
- Pages are ~930 KB each, so pacing defaults to 3 seconds.

# Actor input Schema

## `places` (type: `array`):

Where to search, as Airbnb writes it: 'Paris, France', 'Austin, TX', 'Bali, Indonesia'.

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

Three-letter code, always sent explicitly. This matters more than it looks: left off, Airbnb picks a currency from the EXIT IP — a local run returned Rupiah while the same search with currency=USD returned dollars. Airbnb does not refuse an unknown code either, it silently keeps the previous one, so this actor validates it and reports the currency the page actually rendered.

## `checkin` (type: `string`):

YYYY-MM-DD. Must be given with check-out. WITHOUT dates Airbnb prices a window of its own choosing and the headline price is a multi-night total for that window, not a nightly rate.

## `checkout` (type: `string`):

YYYY-MM-DD. Must be given with check-in.

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

Guest count passed to the search.

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

Child count passed to the search.

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

Airbnb's own price filter, in the currency above.

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

Airbnb's own price filter, in the currency above.

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

Any of: Entire home/apt, Private room, Shared room, Hotel room. Validated here — an unrecognised value would simply be ignored upstream.

## `maxListingsPerPlace` (type: `integer`):

Set 0 to walk until Airbnb stops returning new listings. 18 distinct listings per cursor step.

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

Kept at 1 by default: each search page is about 930 KB of HTML and DataDome runs on this origin.

## `minRequestInterval` (type: `integer`):

Politeness pacing shared across all workers.

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

Optional, but note it changes the DATA here and not just the routing: Airbnb picks a currency from the exit address, and DataDome challenges datacenter exits far more readily than residential ones. Residential is recommended for large runs.

## Actor input object example

```json
{
  "places": [
    "Paris, France",
    "Rome, Italy"
  ],
  "currency": "USD",
  "checkin": "2026-10-10",
  "checkout": "2026-10-13",
  "roomTypes": [
    "Entire home/apt"
  ],
  "maxListingsPerPlace": 60,
  "maxConcurrency": 1,
  "minRequestInterval": 3,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `items` (type: `string`):

One row per scraped record. See the dataset's default view for field definitions.

# 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 = {
    "places": [
        "Paris, France"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapyx/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 = { "places": ["Paris, France"] }

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

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapyx/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/NC8Y8mTqcnFArPIYc/builds/9KDbbkDTBfnFS1uwg/openapi.json
