     [Blog](https://scrapfly.io/blog)   /  [python](https://scrapfly.io/blog/tag/python)   /  [How to Scrape Airbnb Listings and Prices (2026)](https://scrapfly.io/blog/posts/how-to-scrape-airbnb)   # How to Scrape Airbnb Listings and Prices (2026)

 by [Ziad Shamndy](https://scrapfly.io/blog/author/ziad) Aug 14, 2026 22 min read [\#python](https://scrapfly.io/blog/tag/python) 

 [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-scrape-airbnb "Share on LinkedIn") [  ](https://x.com/intent/tweet?url=https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-scrape-airbnb&text=How%20to%20Scrape%20Airbnb%20Listings%20and%20Prices%20%282026%29 "Share on X") [  ](https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-scrape-airbnb "Share on Facebook")    

 

 

Summarize this article with

 [  ](https://chat.openai.com/?q=Summarize%20this%20article%20and%20explain%20how%20Scrapfly%20helps%20me%20scrape%20any%20website%20at%20scale%20and%20bypass%20anti-bot%20systems%20for%20my%20use%20case%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-scrape-airbnb) [  ](https://claude.ai/new?q=Summarize%20this%20article%20and%20explain%20how%20Scrapfly%20helps%20me%20scrape%20any%20website%20at%20scale%20and%20bypass%20anti-bot%20systems%20for%20my%20use%20case%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-scrape-airbnb) [  ](https://x.com/i/grok?text=Summarize%20this%20article%20and%20explain%20how%20Scrapfly%20helps%20me%20scrape%20any%20website%20at%20scale%20and%20bypass%20anti-bot%20systems%20for%20my%20use%20case%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-scrape-airbnb) [  ](https://www.perplexity.ai/search/new?q=Summarize%20this%20article%20and%20explain%20how%20Scrapfly%20helps%20me%20scrape%20any%20website%20at%20scale%20and%20bypass%20anti-bot%20systems%20for%20my%20use%20case%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-scrape-airbnb) [  ](https://www.google.com/search?udm=50&aep=11&q=Summarize%20this%20article%20and%20explain%20how%20Scrapfly%20helps%20me%20scrape%20any%20website%20at%20scale%20and%20bypass%20anti-bot%20systems%20for%20my%20use%20case%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fhow-to-scrape-airbnb) 



         

   **Web Scraping API**Scrape any website with anti-bot bypass, proxy rotation, and JS rendering.

 

 [ Learn More  ](https://scrapfly.io/products/web-scraping-api) [  Docs ](https://scrapfly.io/docs/scrape-api/getting-started) 

 

 

An Airbnb listing page does not contain the price. Scrape the HTML and you get the title, the photos, and the amenities, then a null where the rate belongs. The number lands a second later over Airbnb's GraphQL API, and only if your URL carries dates.

So every Airbnb scraper has two jobs: read the JSON baked into the page, and catch the calls the browser makes after it. This guide does both, then covers the blocking that decides whether a run gets that far.

[How to Scrape Booking.com (2026 Update)Tutorial on how to scrape booking.com hotel and pricing data using Python. How to avoid blocking to web scrape data at scale and other tips.](https://scrapfly.io/blog/posts/how-to-scrape-bookingcom)



[**Airbnb Scraper Code (flat price fields)**github.com/scrapfly/scrapfly-scrapers/tree/main/airbnb-scraper](https://github.com/scrapfly/scrapfly-scrapers/tree/main/airbnb-scraper)

## Key Takeaways

- **The browser is for the price, not the HTML.** A plain request can return a search page with its deferred state; the rate never arrives that way.
- **Parse `data-deferred-state-0`.** Search hits sit under `niobeClientData`.
- **Listing IDs are base64.** Decode `DemandStayListing:<id>` for the room URL.
- **The HTML holds no price.** It arrives over `/api/v3/StaysPdpSections`.
- **No dates, no price.** A bare room URL returns a date prompt instead of a rate.
- **Two more XHRs** carry the guest reviews and 365 days of availability.
- **Airbnb at scale:** [Scrapfly](https://scrapfly.io/products/web-scraping-api) runs the browser and proxies for you.

**Get web scraping tips in your inbox**Trusted by 100K+ developers and 30K+ enterprises. Unsubscribe anytime.







## What Airbnb Data Can You Scrape?

An Airbnb search page gives you listing summaries with IDs, titles, ratings, and stay totals. A listing page adds the full record: description, amenities, photos, host profile, coordinates, price breakdown, reviews, and a year of availability.

### Airbnb Search Result Fields

Search cards carry enough to filter and rank listings before you spend requests on detail pages.

| Field | Example | Notes |
|---|---|---|
| id | `772766711317749064` | Decoded from the base64 `demandStayListing` id |
| url | `https://www.airbnb.com/rooms/772766711317749064` | Built from the decoded id |
| title | `Condo in Panama City Beach` | Property type plus city |
| room\_type | `Calypso 1405 \\| Gulf View \\| Free Activities!` | The card subtitle holds the host's listing name |
| rating | `4.87` | Parsed from the localized rating string |
| review\_count | `61` | Parsed from the same string |
| price\_total | `$1,264 for 7 nights, originally $1,364` | Stay total for your date range, not a nightly rate |

### Airbnb Listing, Host, and Review Fields

Listing pages return a much wider record, and the host block is where the marketplace signals sit.

| Field | Example | Notes |
|---|---|---|
| title | `Calypso 1405 \\| Gulf View \\| Free Activities!` | The host's own listing name |
| room\_type | `Entire condo` | From the page sharing config |
| overview | `["6 guests", "1 bedroom", "4 beds", "2 baths"]` | Capacity summary items |
| amenities | 48 items | Flattened from the amenity groups |
| coordinates | `{"lat": 30.2147, "lng": -85.8745}` | Approximate map position, with a fallback when the section is a stub |
| host | `{"name": "Kristy", "is_superhost": true}` | Includes review count and years hosting |
| price | `{"total": "$1,264 for 7 nights", ...}` | Includes the nightly rate and discount lines |
| reviews | up to 24 records | Reviewer first name, rating, date, and text |
| calendar | 365 days | Per-day availability with minimum stay |

Those field shapes are the parsing targets for the rest of this guide. Before writing a parser, you need URLs that reach the pages holding them.



## How Airbnb Search and Listing URLs Work

Airbnb search and listing URLs are constructable. You never have to automate the search form.

### Building an Airbnb Search URL

A search URL puts a location slug in the path and repeats that location as a `query` parameter. Dates and guests follow:

text```text
https://www.airbnb.com/s/Panama-City-Beach-Florida/homes?query=Panama+City+Beach%2C+Florida&adults=1&checkin=2026-09-10&checkout=2026-09-17
```



The slug is the query with commas and spaces turned into hyphens. There is no usable `page` parameter: Airbnb ignores it and returns the first page again. Pagination is cursor based, and every page's cursor ships inside the first page's embedded JSON, which makes this a [paginated listing](https://scrapfly.io/blog/posts/how-to-scrape-infinite-scroll-load-more-and-paginated-pages) you walk by token rather than by number.

### Listing URLs and the Price-Requires-Dates Rule

Every listing has a stable URL of the form `https://www.airbnb.com/rooms/772766711317749064`.

Load that URL bare and you get the property, the host, the reviews, and the calendar, but no price. Add `check_in`, `check_out`, and `adults` and the price resolves:

text```text
https://www.airbnb.com/rooms/772766711317749064?check_in=2026-09-10&check_out=2026-09-17&adults=2
```



Running both versions of the same room proves the rule. The dated URL returns `$1,264 for 7 nights, originally $1,364`, while the bare URL returns a prompt to add travel dates instead of a rate.



## Project Setup

This guide uses Python 3.10 or later with one package, `scrapfly-sdk`, which handles rendering, blocking, and XHR capture. The rest comes from the standard library: `json`, `re`, `base64`, and `urllib.parse`.

Install it with a single command:

bash```bash
pip install scrapfly-sdk
```



That one package brings its own HTTP stack, so there is no browser or driver to install locally. Rendering happens on Scrapfly's side.

Every request in this guide shares one config. The residential pool keeps prices in US dollars, and the render settings give Airbnb's own API calls time to fire:

python```python
import base64
import json
import re
from typing import Dict, List, Optional
from urllib.parse import quote, urlencode

from scrapfly import ScrapeApiResponse, ScrapeConfig, ScrapflyClient

SCRAPFLY = ScrapflyClient(key="YOUR_SCRAPFLY_KEY")

BASE_CONFIG = {
    "asp": True,                              # bypass Airbnb's anti-bot stack
    "country": "US",
    "proxy_pool": "public_residential_pool",
    "render_js": True,
    "rendering_wait": 5000,
}
```



Get a free API key at [scrapfly.io/register](https://scrapfly.io/register), and see the [Scrape API docs](https://scrapfly.io/docs/scrape-api/getting-started) for the full parameter list. With the client ready, the search scraper comes first.



## How Do You Scrape Airbnb Search Results?

Build the search URL, render it through Scrapfly, and pull the results out of the page's embedded state. Airbnb serializes its GraphQL cache into a script tag with the id `data-deferred-state-0`, and the search response sits inside it.

### Reading the Deferred State

The script tag holds one JSON object whose `niobeClientData` key is a list of `[query_key, payload]` pairs. Search results live in the pair whose key starts with `StaysSearch:`. This is [hidden web data](https://scrapfly.io/blog/posts/how-to-scrape-hidden-web-data), the same pattern behind most modern sites.

Two small helpers cover every page type in this guide, since listing pages use the same container:

python```python
DEFERRED_STATE = re.compile(r'id="data-deferred-state-0"[^>]*>(.*?)</script>', re.DOTALL)

def _niobe(response: ScrapeApiResponse) -> List:
    """pull the GraphQL cache entries out of the page's embedded state"""
    match = DEFERRED_STATE.search(response.content)
    if not match:
        return []
    return json.loads(match.group(1).strip()).get("niobeClientData", [])

def _decode_listing_id(raw: str) -> str:
    """airbnb encodes listing ids as base64 'DemandStayListing:<id>'"""
    try:
        return base64.b64decode(raw + "==").decode().split(":")[-1]
    except Exception:
        return raw
```



The `_decode_listing_id` helper matters because search cards never carry a plain numeric id. They carry a base64 string that decodes to `DemandStayListing:1337190817067371241`, and the room URL needs the number after the colon.

### Parsing and Paginating Search Results

With the helpers in place, the search parser walks the cached search response and flattens each card. The first request also carries every page cursor, so the scraper reads them once and fetches the rest concurrently, skipping IDs it has already seen:

python```python
def build_search_url(query, check_in=None, check_out=None, adults=1, cursor=None) -> str:
    params = {"query": query, "adults": str(adults)}
    if check_in:
        params["checkin"] = check_in
    if check_out:
        params["checkout"] = check_out
    if cursor:
        params["cursor"] = cursor
    slug = quote(query.replace(", ", "-").replace(" ", "-"))
    return f"https://www.airbnb.com/s/{slug}/homes?{urlencode(params)}"

def _search_payloads(response: ScrapeApiResponse) -> List[Dict]:
    """the staysSearch result payloads embedded in a search page"""
    payloads = []
    for entry in _niobe(response):
        if not isinstance(entry, list) or len(entry) < 2:
            continue
        if not entry[0].startswith("StaysSearch:"):
            continue
        payloads.append(entry[1]["data"]["presentation"]["staysSearch"]["results"])
    return payloads

def parse_page_cursors(response: ScrapeApiResponse) -> List[str]:
    """every page's cursor, taken from the first page's embedded JSON"""
    for payload in _search_payloads(response):
        cursors = (payload.get("paginationInfo") or {}).get("pageCursors") or []
        if cursors:
            return cursors
    return []

def parse_search(response: ScrapeApiResponse) -> List[Dict]:
    """parse search results from an Airbnb search page"""
    results = []
    for payload in _search_payloads(response):
        for item in payload["searchResults"]:
            rating = review_count = None
            match = re.match(r"([\d.]+)\s*\((\d+)\)", item.get("avgRatingLocalized") or "")
            if match:
                rating, review_count = float(match.group(1)), int(match.group(2))
            listing_id = _decode_listing_id(item.get("demandStayListing", {}).get("id", ""))
            results.append({
                "id": listing_id,
                "url": f"https://www.airbnb.com/rooms/{listing_id}",
                "title": item.get("title"),
                "room_type": item.get("subtitle"),
                "rating": rating,
                "review_count": review_count,
                "price_total": item.get("structuredDisplayPrice", {}).get("primaryLine", {}).get("accessibilityLabel"),
            })
    return results

async def scrape_search(query, check_in=None, check_out=None, adults=1, max_pages=3) -> List[Dict]:
    """scrape Airbnb search results for a location query"""
    first_url = build_search_url(query, check_in, check_out, adults)
    first_page = await SCRAPFLY.async_scrape(ScrapeConfig(first_url, **BASE_CONFIG, rendering_wait=5000))

    results = parse_search(first_page)
    if not results:
        return []
    seen = {item["id"] for item in results}

    # the first cursor points back at the page already scraped
    cursors = parse_page_cursors(first_page)[1:max_pages]
    to_scrape = [
        ScrapeConfig(
            build_search_url(query, check_in, check_out, adults, cursor),
            **BASE_CONFIG,
            rendering_wait=5000,
        )
        for cursor in cursors
    ]
    async for response in SCRAPFLY.concurrent_scrape(to_scrape):
        # Airbnb reshuffles results between requests, so pages overlap
        page_results = [item for item in parse_search(response) if item["id"] not in seen]
        seen.update(item["id"] for item in page_results)
        results.extend(page_results)
    return results
```



An empty first page ends the run early. Airbnb answers a challenged request with a normal HTTP 200 and no search entries, so treating "zero listings" as a stop condition catches that case. The `seen` set matters just as much: Airbnb reshuffles results between requests, so cursor pages overlap and the same listing can appear twice.

Run it against a location and a date range:

python```python
import asyncio

async def run():
    results = await scrape_search(
        "Panama City Beach, Florida",
        check_in="2026-09-10",
        check_out="2026-09-17",
        max_pages=3,
    )
    print(json.dumps(results[:2], indent=2, ensure_ascii=False))

if __name__ == "__main__":
    asyncio.run(run())
```



Three cursor pages returned 48 listings with no duplicate IDs, each one already carrying a usable room URL:

json```json
[
  {
    "id": "772766711317749064",
    "url": "https://www.airbnb.com/rooms/772766711317749064",
    "title": "Condo in Panama City Beach",
    "room_type": "Calypso 1405 | Gulf View | Free Activities!",
    "rating": 4.87,
    "review_count": 61,
    "price_total": "$1,264 for 7 nights, originally $1,364"
  },
  {
    "id": "1061095453380217770",
    "url": "https://www.airbnb.com/rooms/1061095453380217770",
    "title": "Condo in Panama City Beach",
    "room_type": "Ohana Sunrise and Origin",
    "rating": 4.92,
    "review_count": 61,
    "price_total": "$978 for 7 nights, originally $1,396"
  }
]
```



Note that `title` is the property type and city, while the host's own listing name lands in `room_type`. Those bare room URLs feed the listing scraper in the next section. Pass the same check-in and check-out values to `scrape_properties`, which appends them before fetching each room.



Scrapfly

#### Scale your web scraping effortlessly

Scrapfly handles proxies, browsers, and anti-bot bypass — so you can focus on data.

[Try Free →](https://scrapfly.io/register)## How Do You Scrape an Airbnb Listing Page?

A listing page splits its data in two. Static content sits in the deferred state, and the parts that depend on your dates arrive afterwards as GraphQL calls from the browser.

### Finding the Calls the Page Makes for Itself

Open a room URL with DevTools on the Network tab, filtered to Fetch/XHR, and three calls under `/api/v3/` carry the data worth having:

- `StaysPdpSections` returns the section tree again, this time with the price filled in.
- `StaysPdpReviewsQuery` returns the guest reviews.
- `PdpAvailabilityCalendar` returns twelve months of day-level availability.

This is the standard [hidden API](https://scrapfly.io/blog/posts/how-to-scrape-hidden-apis) discovery flow, and [browser DevTools](https://scrapfly.io/blog/answers/browser-developer-tools-in-web-scraping) are the only tool it needs.

Scrapfly captures those responses for you. Add `wait_for_selector="xhr:StaysPdpReviewsQuery"` and the render holds until the reviews call fires, then hands you every captured request in `browser_data`. Not every listing fires that call, so catch `ERR::SCRAPE::DOM_SELECTOR_NOT_FOUND` and retry without the wait rather than losing the price too:

python```python
async def scrape_properties(urls, check_in=None, check_out=None, adults=1) -> List[Dict]:
    """scrape Airbnb property pages including price, reviews, and calendar

    check_in/check_out (YYYY-MM-DD) are required for the price fields
    """
    results = []
    for url in urls:
        params = {"adults": str(adults)}
        if check_in:
            params["check_in"] = check_in
        if check_out:
            params["check_out"] = check_out
        full_url = url + ("&" if "?" in url else "?") + urlencode(params)
        config = dict(BASE_CONFIG, rendering_wait=15000)
        try:
            response = await SCRAPFLY.async_scrape(
                ScrapeConfig(full_url, wait_for_selector="xhr:StaysPdpReviewsQuery", **config)
            )
        except ScrapflyScrapeError as e:
            if e.code != "ERR::SCRAPE::DOM_SELECTOR_NOT_FOUND":
                raise
            # some listings never fire the reviews call; keep the price and calendar
            response = await SCRAPFLY.async_scrape(ScrapeConfig(full_url, **config))
        results.append(parse_property(response))
    return results

def _xhr_payloads(response: ScrapeApiResponse, operation: str) -> List[Dict]:
    """decode every captured XHR body for one Airbnb GraphQL operation"""
    payloads = []
    for call in response.scrape_result.get("browser_data", {}).get("xhr_call", []):
        if f"/api/v3/{operation}" not in call.get("url", ""):
            continue
        body = (call.get("response") or {}).get("body")
        if body:
            payloads.append(json.loads(body))
    return payloads
```



One `_xhr_payloads` helper now serves the price, review, and calendar parsers below. Each of them names a different operation and reads a different branch of the response. Doing the same with a local driver means wiring up [background request interception](https://scrapfly.io/blog/posts/web-scraping-background-requests-with-headless-browsers-and-python) yourself.

### Parsing Listing Details and Host Data

The static half of the page hangs off the `StaysPdpSections` entry in the deferred state. Its `pdpPresentation` node holds the title, description, amenities, photos, capacity, and host, while a sibling `sections` list holds the map coordinates.

Read the host from `pdpPresentation.hostInfo.passportData` rather than the `MEET_YOUR_HOST` section. That section ships as an empty stub on many listings, and so do `REVIEWS_DEFAULT`, `TITLE_DEFAULT`, and `AMENITIES_DEFAULT`. `LOCATION_PDP` can be a stub too, which is why the coordinates fall back to `pdpPresentation.location`:

python```python
def parse_property(response: ScrapeApiResponse) -> Dict:
    """parse property data from an Airbnb listing page"""
    pdp_data, listing_id = None, ""
    for entry in _niobe(response):
        if not isinstance(entry, list) or len(entry) < 2 or not entry[0].startswith("StaysPdpSections"):
            continue
        pdp_data = entry[1]
        raw_id = re.search(r'demandStayListingId":"([^"]+)"', entry[0]).group(1)
        listing_id = _decode_listing_id(raw_id)
        break
    if not pdp_data:
        raise ValueError("StaysPdpSections data not found")

    pdp = pdp_data["data"]["node"]["pdpPresentation"]
    sections = pdp_data["data"]["presentation"]["stayProductDetailPage"]["sections"]

    passport = (pdp.get("hostInfo") or {}).get("passportData") or {}
    host = {
        "name": passport.get("name"),
        "is_superhost": passport.get("isSuperhost"),
        "is_verified": passport.get("isVerified"),
        "stats": {s["label"]: s["value"] for s in passport.get("stats", [])},
    } if passport else None

    coordinates = location = rating = review_count = None
    for section in sections["sections"]:
        data = section.get("section") or {}
        if section["sectionComponentType"] != "LOCATION_PDP":
            continue
        # the section ships as an empty stub on many listings
        if data.get("lat") is not None:
            coordinates = {"lat": data["lat"], "lng": data["lng"]}
        location = location or data.get("subtitle")

    pdp_location = pdp.get("location") or {}
    if coordinates is None and pdp_location.get("latitude") is not None:
        coordinates = {"lat": pdp_location["latitude"], "lng": pdp_location["longitude"]}
    location = location or pdp_location.get("subtitle")

    stats = pdp.get("quality", {}).get("listingRatingStats", {}).get("overallRatingStats", {})
    rating = stats.get("ratingAverage")
    review_count = int(stats.get("ratingCount") or 0) or None

    return {
        "url": response.context.get("url", f"https://www.airbnb.com/rooms/{listing_id}"),
        "id": listing_id,
        "title": pdp["title"]["content"]["localizedString"],
        "description": pdp["descriptions"]["longDescriptionHtml"]["localizedString"],
        "room_type": sections["metadata"]["sharingConfig"]["propertyType"],
        "overview": pdp["overview"]["items"],
        "amenities": [a["title"] for g in pdp["amenities"]["seeAllAmenitiesGroups"] for a in g["amenities"]],
        "images": [e["node"]["image"]["uri"] for e in pdp["heroMedia"]["edges"]],
        "host": host,
        "coordinates": coordinates,
        "location": location,
        "person_capacity": pdp["personCapacity"],
        "rating": rating,
        "review_count": review_count,
        "price": parse_price(response),
        "calendar": parse_calendar(response),
        "reviews": parse_reviews(response) or None,
    }
```



Ratings come from `quality.listingRatingStats` for the same reason. The rating is on the node even when the review section is a stub, and `ratingCount` arrives as a string, so cast it before storing.

Here is the static half of one record, with the long fields trimmed:

json```json
{
  "id": "772766711317749064",
  "title": "Calypso 1405 | Gulf View | Free Activities!",
  "room_type": "Entire condo",
  "overview": ["6 guests", "1 bedroom", "4 beds", "2 baths"],
  "amenities": ["Beach view", "Hair dryer", "Shampoo", "Conditioner", "Body soap", "Hot water"],
  "host": {
    "name": "Kristy",
    "is_superhost": true,
    "is_verified": true,
    "stats": {"Reviews": "8517", "Rating": "4.84", "Years hosting": "6"}
  },
  "coordinates": {"lat": 30.2147, "lng": -85.8745},
  "location": "Panama City Beach, Florida, United States",
  "person_capacity": 6,
  "rating": 4.87,
  "review_count": 61
}
```



The host stats tell you the difference between a private host and a property manager. This one carries 8,517 reviews across a portfolio, which is a strong signal for market analysis.

### Getting the Fee-Inclusive Price

Price is the field most Airbnb scrapers get wrong. It never appears in the server-rendered state, even when the URL carries dates. The browser refetches the section tree over `StaysPdpSections` once the calendar resolves.

The captured response holds a `BOOK_IT_SIDEBAR` section with the display price and, under `explanationData`, the line items behind it:

python```python
def parse_price(response: ScrapeApiResponse) -> Optional[Dict]:
    """read the fee-inclusive price from the captured StaysPdpSections XHR"""
    for data in _xhr_payloads(response, "StaysPdpSections"):
        sections = data["data"]["presentation"]["stayProductDetailPage"]["sections"]["sections"]
        for section in sections:
            if section["sectionComponentType"] != "BOOK_IT_SIDEBAR":
                continue
            price = ((section.get("section") or {}).get("structuredDisplayPrice") or {}).get("primaryLine")
            if not price:
                continue
            groups = (section["section"]["structuredDisplayPrice"].get("explanationData") or {}).get("priceDetails", [])
            return {
                "total": price.get("accessibilityLabel"),
                "discounted": price.get("discountedPrice") or price.get("price"),
                "original": price.get("originalPrice"),
                "qualifier": price.get("qualifier"),
                "breakdown": [
                    {"label": item["description"], "amount": item["priceString"]}
                    for group in groups for item in (group.get("items") or [])
                ],
            }
    return None
```



The breakdown is what makes this worth capturing. It separates the nightly rate from the discounts, so you can compare listings on the same basis instead of on a headline number. Read the labels rather than assuming fixed rows: groups can arrive without an `items` list, and the lines vary by listing and locale. In the run below, three listings returned three, one, and three lines:

json```json
{
  "total": "$1,264 for 7 nights, originally $1,364",
  "discounted": "$1,264",
  "original": "$1,364",
  "qualifier": "for 7 nights",
  "breakdown": [
    {"label": "7 nights x $194.76", "amount": "$1,363.32"},
    {"label": "Weekly stay discount", "amount": "-$99.48"},
    {"label": "Price after discount", "amount": "$1,263.84"}
  ]
}
```



Drop the date parameters and the response carries a date prompt instead of a usable total. Always pass a date range when price is what you are after.

[Web Scraping Graphql with PythonIntroduction to web scraping graphql powered websites. How to create graphql queries in python and what are some common challenges.](https://scrapfly.io/blog/posts/web-scraping-graphql-with-python)

### Scraping Airbnb Reviews

Reviews come from their own operation, `StaysPdpReviewsQuery`, and that is the call the scraper already waits on. Each review carries the reviewer's first name, a rating, a localized date, the text, and any host response:

python```python
def parse_reviews(response: ScrapeApiResponse) -> List[Dict]:
    """collect guest reviews from the captured StaysPdpReviewsQuery XHR"""
    reviews = []
    for data in _xhr_payloads(response, "StaysPdpReviewsQuery"):
        for review in data["data"]["presentation"]["stayProductDetailPage"]["reviews"]["reviews"]:
            reviews.append({
                "id": review["id"],
                "reviewer": review["reviewer"]["firstName"],
                "rating": review["rating"],
                "date": review["localizedDate"],
                "text": review["comments"],
                "response": review.get("response"),
            })
    return reviews
```



The first call returned 24 reviews for the sample listing, which is the page's own first batch rather than the full history. Other listings in the same run returned 15, so treat 24 as a ceiling:

json```json
[
  {
    "id": "1734873429881483978",
    "reviewer": "Aylin",
    "rating": 5,
    "date": "3 weeks ago",
    "text": "I've stayed in many Airbnbs around the world ... this was by far the cleanest Airbnb I have ever stayed in.<br/>The location is fantastic, and the view from the balcony is an absolute 10/10.",
    "response": null
  }
]
```



Review text keeps Airbnb's inline HTML, so strip `<br/>` tags before any text analysis. To go past the first batch, replay the same GraphQL request with a higher `offset` in its variables.

### Reading the Availability Calendar

The `PdpAvailabilityCalendar` call returns twelve months of days in one response, with no extra requests. Each day states whether it is bookable and what stay length it allows:

python```python
def parse_calendar(response: ScrapeApiResponse) -> List[Dict]:
    """read day-level availability from the captured PdpAvailabilityCalendar XHR"""
    days = []
    for data in _xhr_payloads(response, "PdpAvailabilityCalendar"):
        for month in data["data"]["merlin"]["pdpAvailabilityCalendar"]["calendarMonths"]:
            for day in month["days"]:
                days.append({
                    "date": day["calendarDate"],
                    "available": day["available"],
                    "min_nights": day["minNights"],
                    "max_nights": day["maxNights"],
                })
    return days
```



One listing yields 365 dated records in a single field:

json```json
[
  {"date": "2026-08-01", "available": false, "min_nights": 1, "max_nights": 1125},
  {"date": "2026-08-02", "available": false, "min_nights": 1, "max_nights": 1125},
  {"date": "2026-08-03", "available": false, "min_nights": 1, "max_nights": 1125}
]
```



Store each capture with an ISO timestamp, because occupancy is the difference between two captures. A blocked day can mean a booking, a host block, or a minimum-stay rule. The same snapshot habit drives any [price tracker](https://scrapfly.io/blog/posts/how-to-build-a-price-tracker-using-python-web-scraping).

With search, details, price, reviews, and availability covered, what remains is keeping the scraper alive at volume.



## Why Does Airbnb Block Scrapers?

Airbnb treats its marketplace data as a competitive asset, so it defends it hard. Detection runs on IP reputation, browser fingerprinting, and request volume. The answer to all three is a block, not a CAPTCHA loop.

Four layers do most of the damage:

- **IP reputation.** Sustained scraping from one address gets banned within a few hundred requests, and datacenter ranges go faster.
- **JavaScript rendering.** A plain HTTP request may still return the deferred state, but the priced XHR responses only exist once the page's own scripts run.
- **Generated DOM classes.** Airbnb rebuilds class names per deploy, so selector-based parsers break silently.
- **Anti-bot cookies.** Airbnb sets fingerprint-carrying cookies such as `datadome`, `everest_cookie`, and `bev`; which ones appear varies by session and edge.

Scrapfly's Anti Scraping Protection answers all four. Setting `asp=True` rotates residential IPs, matches TLS and browser fingerprints, manages the cookies, and clears challenge pages before the HTML reaches your parser.

For the detection techniques behind those defenses, see our [how to bypass anti-bot protection](https://scrapfly.io/blog/posts/how-to-bypass-anti-bot-protection-when-web-scraping) guide.



## Is There an Airbnb API?

Airbnb does not offer a public data API for general developers. The honest answer to "does Airbnb have an API" is yes for approved partners and no for everyone else.

The partner API, often called the Homes API, connects Host Services and property management software. The [Airbnb API Terms](https://www.airbnb.com/help/article/3418) restrict access to vetted partners and cover host functionality, not open data.

Third-party providers resell aggregated short-term rental data under their own licensing terms. Scraping the public listing pages stays the practical route when you need what the site shows and you are not an approved partner.

Without an API to lean on, the scraper has to survive Airbnb's defenses on its own. That is the part worth handing to a dedicated tool.

## Scraping Airbnb With Scrapfly



Scrapfly provides web scraping, screenshot, and extraction APIs for data collection at scale. Every part of this Airbnb scraper depends on it, from the rendered search page to the captured GraphQL calls that carry the price.

- **Anti Scraping Protection** clears Airbnb's fingerprint and cookie checks with a single `asp=True` flag.
- **Residential proxies in 190+ countries** keep prices and currency matched to your target market.
- **JavaScript rendering with XHR capture** hands you Airbnb's own API responses in `browser_data`.
- **`wait_for_selector="xhr:..."`** holds the render until the call you need has fired.
- **A Python SDK with async support** runs listing pages concurrently without extra plumbing.

The full scraper above is a little over 200 lines because Scrapfly absorbs the proxy rotation, the browser, and the blocking.



### Web Scraping API

Scrape any website with our powerful API. Anti-bot bypass, JavaScript rendering, and rotating proxies built-in.



[Try Web Scraping API](https://scrapfly.io/docs/scrape-api/getting-started)



## FAQ

Is it legal to scrape Airbnb data?Courts treat public listing data differently from account-gated data, but Airbnb's terms still apply. Don't redistribute personal data, and see our [is web scraping legal](https://scrapfly.io/is-web-scraping-legal) guide.







Why is the price missing from my scraped data?Airbnb only resolves a price when the room URL carries `check_in` and `check_out` parameters. It also arrives by XHR rather than in the HTML, so you need JavaScript rendering plus captured browser data.







Can I scrape Airbnb with requests and BeautifulSoup?[BeautifulSoup](https://scrapfly.io/blog/posts/web-scraping-with-python-beautifulsoup) parses the deferred state fine once you have it. Getting it reliably is the problem: the price arrives over XHR rather than in the HTML, and single-IP volume gets banned quickly.







How many reviews does one listing request return?The first `StaysPdpReviewsQuery` call returns the page's opening batch, up to 24 reviews and sometimes fewer. Replay the same request with a higher offset to page through the rest.







How do I export Airbnb data to CSV?Collect the normalized records into a list of dicts and write them with [pandas](https://pandas.pydata.org/): `pandas.DataFrame(rows).to_csv("airbnb.csv", index=False)`. Keep the capture timestamp as its own column so price changes stay comparable over time.







Are there alternatives to Airbnb for short-term rental data?Booking.com is the closest lodging equivalent, with the same date-driven pricing model. For property data more broadly, see the guides on [Zillow](https://scrapfly.io/blog/posts/how-to-scrape-zillow) and [Redfin](https://scrapfly.io/blog/posts/how-to-scrape-redfin).









## Summary

Airbnb hands you its data in two places. The deferred state script tag carries search results and the static half of every listing. The site's own GraphQL calls carry price, reviews, and a year of availability.

Parsing that structure beats selectors by a wide margin. Class names change per deploy, while `niobeClientData` and its section types have stayed put. A moved key also raises a clear error instead of quietly returning nothing.

The two rules worth keeping are simple. Always pass dates when you want a price, and always capture the XHR calls rather than hunting for their data in the HTML.



Legal Disclaimer and PrecautionsThis tutorial covers popular web scraping techniques for education. Interacting with public servers requires diligence and respect:

- Do not scrape at rates that could damage the website.
- Do not scrape data that's not available publicly.
- Do not store PII of EU citizens protected by GDPR.
- Do not repurpose *entire* public datasets which can be illegal in some countries.

Scrapfly does not offer legal advice but these are good general rules to follow. For more you should consult a lawyer.

 

   [  Add as a preferred source ](https://google.com/preferences/source?q=scrapfly.io) Table of Contents















 

  Table of Contents- [Key Takeaways](#key-takeaways)
- [What Airbnb Data Can You Scrape?](#what-airbnb-data-can-you-scrape)
- [Airbnb Search Result Fields](#airbnb-search-result-fields)
- [Airbnb Listing, Host, and Review Fields](#airbnb-listing-host-and-review-fields)
- [How Airbnb Search and Listing URLs Work](#how-airbnb-search-and-listing-urls-work)
- [Building an Airbnb Search URL](#building-an-airbnb-search-url)
- [Listing URLs and the Price-Requires-Dates Rule](#listing-urls-and-the-price-requires-dates-rule)
- [Project Setup](#project-setup)
- [How Do You Scrape Airbnb Search Results?](#how-do-you-scrape-airbnb-search-results)
- [Reading the Deferred State](#reading-the-deferred-state)
- [Parsing and Paginating Search Results](#parsing-and-paginating-search-results)
- [How Do You Scrape an Airbnb Listing Page?](#how-do-you-scrape-an-airbnb-listing-page)
- [Finding the Calls the Page Makes for Itself](#finding-the-calls-the-page-makes-for-itself)
- [Parsing Listing Details and Host Data](#parsing-listing-details-and-host-data)
- [Getting the Fee-Inclusive Price](#getting-the-fee-inclusive-price)
- [Scraping Airbnb Reviews](#scraping-airbnb-reviews)
- [Reading the Availability Calendar](#reading-the-availability-calendar)
- [Why Does Airbnb Block Scrapers?](#why-does-airbnb-block-scrapers)
- [Is There an Airbnb API?](#is-there-an-airbnb-api)
- [Scraping Airbnb With Scrapfly](#scraping-airbnb-with-scrapfly)
- [FAQ](#faq)
- [Summary](#summary)
 
    Join the Newsletter  Get monthly web scraping insights 

 

  



Scale Your Web Scraping

Anti-bot bypass, browser rendering, and rotating proxies, all in one API. Start with 1,000 free credits.

  No credit card required  1,000 free API credits  Anti-bot bypass included 

 [Start Free](https://scrapfly.io/register) [View Docs](https://scrapfly.io/docs/onboarding) 

 Not ready? Get our newsletter instead. 

 

 ## Related Articles

 [     

 blocking 

### 5 Tools to Scrape Without Blocking and How it All Works

Tutorial on how to avoid web scraper blocking. What is javascript and TLS (JA3) fingerprinting and what role request hea...

 

 ](https://scrapfly.io/blog/posts/how-to-scrape-without-getting-blocked-tutorial) [     

 python scrapeguide 

### How to Scrape Marriott Hotel Prices and Availability

Learn how to scrape Marriott hotel prices and room availability with Python in 2026, including search inputs, the JSON b...

 

 ](https://scrapfly.io/blog/posts/how-to-scrape-marriott) [  

 python scrapeguide 

### How to Scrape Google Search Results in 2026

In this scrape guide we'll be taking a look at how to scrape Google Search - the biggest index of public web. We'll cov...

 

 ](https://scrapfly.io/blog/posts/how-to-scrape-google) 

  



   



 Scale your web scraping effortlessly, **1,000 free credits** [Start Free](https://scrapfly.io/register)