Airbnb listing pages carry exactly the kind of pricing intelligence that powers travel research, market analysis, and competitive monitoring: a nightly rate, a listing title, a guest rating, and the public location a host advertises. The problem is that Airbnb renders those pages client-side and defends them against automated traffic hard, so a plain HTTP request hands you an empty shell with no price in it.

This guide shows you how to scrape Airbnb prices with Python the reliable way. You build a small, runnable scraper that fetches a rendered listing page through the Crawling API, parses the fields you want with BeautifulSoup, and prints a clean structured record. We keep the walkthrough scoped to public listing data, and the legality section near the end is not boilerplate, so read it before you point this at any real volume.

What you will build

A Python script that takes a public Airbnb listing URL, retrieves the rendered HTML through the Crawling API, and extracts a structured record for the listing. We will target a single room URL as the running example and pull these fields:

  • Title the listing name shown at the top of the page.
  • Price the nightly rate for the dates you request.
  • Rating the public guest rating, like "4.92".
  • Location the area Airbnb shows publicly for the listing.

Why a plain fetch fails on Airbnb

If you request an Airbnb room URL with a bare HTTP client, you get a response with status 200 and almost none of the listing data in the body. Two things work against you. First, Airbnb renders its listing content in the browser with JavaScript, so the initial HTML is a shell that only fills in after the page's scripts run. Second, Airbnb flags automated traffic quickly: datacenter IPs and request patterns that do not look like a real browser get challenged or blocked before they reach the rendered content.

There is a third wrinkle specific to pricing. Since Airbnb's pricing rework, the nightly rate only appears once the page has check-in and check-out dates plus a guest count. Pricing is dynamic: the same listing shows a different number for different dates, weekends, and seasons. So a working Airbnb price scraper needs a browser that renders the page, an IP the platform reads as a real visitor, and date parameters in the URL so the price renders at all.

You can assemble that yourself with a headless browser plus a pool of rotating residential proxies, but stitching those together and keeping them healthy is most of the work. The Crawling API folds the hard parts into a single call: you send it the listing URL with a JavaScript token, it renders the page behind a trusted IP, and it returns finished HTML for you to parse.

Why the JS token

Crawlbase offers two token types. The normal token fetches static HTML; the JavaScript (JS) token renders the page in a real browser first. Airbnb is client-side rendered, so you need the JS token here. Using the normal token returns the same empty shell a plain fetch would, with no price to parse out of it.

Prerequisites

You need a few things in place before writing any code. None of them take long.

Basic Python. You should be comfortable writing and running a Python script and installing packages with pip. If you are new to parsing HTML, our guide on how to use BeautifulSoup in Python covers the selector basics this tutorial assumes.

Python 3.8 or later. Confirm your version with python --version. If you do not have it, install it from python.org or through a distribution like Anaconda.

A Crawlbase account and JS token. Sign up, open your dashboard, and copy your JavaScript (JS) token from the account docs page. Treat the token like a password: it authenticates your requests, so keep it out of version control.

Set up the project

Create a virtual environment so project dependencies stay isolated, then install the two libraries the scraper needs.

bash
python --version

python -m venv airbnb_env
source airbnb_env/bin/activate

pip install crawlbase beautifulsoup4

On Windows, activate the environment with airbnb_env\Scripts\activate instead of the source line. Two dependencies do the work: crawlbase is the official client for the Crawling API, and beautifulsoup4 parses the returned HTML so you can pull out individual fields by CSS selector.

Step 1: Fetch the rendered listing

Start by getting the finished page. Import the CrawlingAPI class, initialize it with your JS token, and request the listing URL with dates attached. Checking the status code before you parse keeps failures loud instead of silent.

python
from crawlbase import CrawlingAPI

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"})

def crawl(page_url):
    options = {"ajax_wait": "true", "page_wait": 5000}
    response = api.get(page_url, options)
    if response["status_code"] == 200:
        return response["body"].decode("latin1")
    print(f"Request failed: {response['status_code']}")
    return None

if __name__ == "__main__":
    page_url = (
        "https://www.airbnb.com/rooms/721540609203378406"
        "?check_in=2026-07-09&check_out=2026-07-12&adults=2"
    )
    html = crawl(page_url)
    print(html[:500] if html else "No HTML returned")

The check_in, check_out, and adults query parameters are not optional for pricing. Without them the page renders, but the nightly rate stays hidden, so the price field comes back empty no matter how good your selector is. The two wait options matter for a client-rendered target like this: ajax_wait tells the API to wait for asynchronous content to finish loading, and page_wait holds for a fixed number of milliseconds after load so late-rendering elements appear before the page is captured. Five seconds is a reasonable start; raise it if the price comes back blank. Airbnb sends pages in a range of encodings, so decoding with latin1 avoids the byte errors a strict UTF-8 decode can throw. Run the script and you should see real listing markup, not the empty shell a plain fetch returns.

Crawlbase Airbnb Scraper

Airbnb needs a rendered page behind a trusted IP, in one call. The Crawling API takes a JS token, runs the page in a real browser, rotates through residential IPs server-side, and hands you finished HTML, so you skip running a headless fleet and a proxy pool yourself. Point it at a public listing on the free tier first.

Step 2: Parse the listing fields with BeautifulSoup

With rendered HTML in hand, load it into BeautifulSoup and pull each field by its selector. Airbnb lays the core details out in stable section containers, so you can map title, price, rating, and location to individual selectors. Wrap the whole extraction in a try/except so one missing field does not crash the run.

python
from bs4 import BeautifulSoup

def text_of(soup, selector):
    el = soup.select_one(selector)
    return el.get_text(strip=True) if el else None

def scrape_listing(html):
    soup = BeautifulSoup(html, "html.parser")

    return {
        "title": text_of(soup, 'div[data-section-id="TITLE_DEFAULT"] h1'),
        "price": text_of(soup, 'div[data-testid="book-it-default"] span > span'),
        "rating": text_of(soup, 'div[data-testid="pdp-reviews-highlight-banner-host-rating"] span'),
        "location": text_of(soup, 'div[data-section-id="LOCATION_DEFAULT"] h3'),
    }

The text_of helper does two useful things at once: it queries a single element and returns None when the element is missing, instead of throwing on a .get_text() call against nothing. That keeps the extraction resilient when one field is absent, which is common since a brand-new listing may have no rating yet. Airbnb's data-section-id and data-testid attributes are more stable than its hashed CSS class names, so prefer those data attributes as anchors wherever you can.

Selectors drift

Airbnb's markup, especially its hashed class names, changes without notice. Treat the selectors above as a starting template, not a contract. When a field comes back as None, re-inspect the live page in your browser's dev tools and update the selector. Periodic selector maintenance is normal for any production scraper, not a sign something is broken.

Step 3: Put it together

Now wire the fetch and the parse into one runnable script. Fetch the rendered HTML, hand it to the parser, and print the structured record as JSON.

python
import json
from crawlbase import CrawlingAPI
from bs4 import BeautifulSoup

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"})

def crawl(page_url):
    options = {"ajax_wait": "true", "page_wait": 5000}
    response = api.get(page_url, options)
    if response["status_code"] == 200:
        return response["body"].decode("latin1")
    print(f"Request failed: {response['status_code']}")
    return None

def text_of(soup, selector):
    el = soup.select_one(selector)
    return el.get_text(strip=True) if el else None

def scrape_listing(html):
    soup = BeautifulSoup(html, "html.parser")
    return {
        "title": text_of(soup, 'div[data-section-id="TITLE_DEFAULT"] h1'),
        "price": text_of(soup, 'div[data-testid="book-it-default"] span > span'),
        "rating": text_of(soup, 'div[data-testid="pdp-reviews-highlight-banner-host-rating"] span'),
        "location": text_of(soup, 'div[data-section-id="LOCATION_DEFAULT"] h3'),
    }

def main():
    page_url = (
        "https://www.airbnb.com/rooms/721540609203378406"
        "?check_in=2026-07-09&check_out=2026-07-12&adults=2"
    )
    html = crawl(page_url)
    if not html:
        return
    data = scrape_listing(html)
    print(json.dumps(data, indent=2))

if __name__ == "__main__":
    main()

What the output looks like

Run the full script with python scraper.py and you get a clean structured record for the listing, ready to write to JSON, CSV, or a database.

json
{
  "title": "Lovely Studio with Burj Khalifa views from Balcony",
  "price": "$187 per night",
  "rating": "4.92",
  "location": "Dubai, United Arab Emirates"
}

Scraping prices across dates

One date range is a demo; the interesting pricing work compares the same listing across many dates, because Airbnb prices move with the calendar. The shape stays the same: keep a list of date pairs, rebuild the URL for each, fetch it through the Crawling API, and parse it with the same function. Because the listing structure does not change, the parser you already wrote works for every date without changes.

python
room_id = "721540609203378406"
date_ranges = [
    ("2026-07-09", "2026-07-12"),
    ("2026-08-13", "2026-08-16"),
]

results = []
for check_in, check_out in date_ranges:
    url = (
        f"https://www.airbnb.com/rooms/{room_id}"
        f"?check_in={check_in}&check_out={check_out}&adults=2"
    )
    html = crawl(url)
    if html:
        row = scrape_listing(html)
        row["check_in"] = check_in
        results.append(row)

with open("airbnb_prices.json", "w") as f:
    json.dump(results, f, indent=2)

To find listing URLs in the first place, you can fetch Airbnb's public search results with the same pattern, collect the room links, and then visit each one with dates attached. Just keep the volume reasonable and respect the rate expectations covered below. If you plan to feed this into a pricing model, our guide on using web scraping for price intelligence covers how teams turn a stream of rates into something decision-ready.

Staying unblocked

Even with rendering handled, Airbnb watches for scraper-shaped traffic. A few habits keep a run healthy, and they apply to any hard, JavaScript-heavy target.

  • Pace your requests. Hammering one listing across dozens of date ranges in a tight loop is the fastest way to get throttled. Spread requests out and vary your targets instead of crawling one path at full speed.
  • Lean on rotation. A pool of residential IPs spreads requests across many real-user addresses so no single one trips a rate limit. The Crawling API handles this for you; if you roll your own stack, this is the part to get right.
  • Read the status codes. A run that starts returning challenges or errors is telling you the current rate or IP tier is no longer enough. Treat that as signal to back off, not noise to ignore.

For the broader playbook, see how to scrape websites without getting blocked and the deeper dive on how to bypass captchas while web scraping. Because Airbnb is so JavaScript-heavy, our walkthrough on how to crawl JavaScript websites is also worth a read. If you would rather route your own traffic through a rotating pool instead of using the managed API, the Smart AI Proxy gives you the same residential IP rotation as a drop-in proxy endpoint.

Whether scraping Airbnb is allowed depends on Airbnb's terms of service, your jurisdiction, and what you do with the data. Airbnb's terms specifically restrict automated access, so scraping can run against those terms regardless of how careful your tooling is. None of the code here changes that; it just makes the technical part work. Read the Airbnb Terms of Service and its robots.txt, and treat both as the boundary for what you collect.

A few lines worth holding to. Collect only public listing data: the title, the nightly price, the public rating, and the location Airbnb shows on the listing without an account. Respect Airbnb's stated rate expectations and keep your request volume low enough that you are not straining its servers. This guide is deliberately scoped to public listing pages because that is the line that keeps the work defensible.

What it does not cover is just as important. Do not collect host or guest personal data, contact information, or messages. Do not touch login-walled pages, account or booking data behind a sign-in, or anything that requires bypassing authentication to reach. Those are out of scope here and run against Airbnb's terms. For commercial or large-volume use, the right path is an official partnership or API agreement with Airbnb, not a cleverer scraper. Public listing data only.

Recap

Key takeaways

  • Airbnb is client-side rendered. A plain fetch returns an empty shell, so you must render the page before you parse it.
  • Pricing needs dates. Pass check_in, check_out, and adults in the URL or the nightly rate never renders; prices are dynamic and move with the calendar.
  • You need rendering and a trusted IP together. The Crawling API with a JS token does both in one call; ajax_wait and page_wait control how long it waits for content.
  • BeautifulSoup does the extraction. Anchor on data-section-id and data-testid attributes for title, price, rating, and location, and expect selectors to drift.
  • Stay on public data. Respect Airbnb's ToS and robots.txt, prefer an official partnership for commercial use, and never touch host or guest personal data, messages, or login-walled pages.

Frequently Asked Questions (FAQs)

Why does a plain fetch return no price from Airbnb?

Two reasons. First, Airbnb renders its listing content client-side with JavaScript, so the raw HTML is a shell that only fills in after the page's scripts run. Second, the nightly rate only renders once the URL carries check-in and check-out dates plus a guest count. A bare HTTP request with no dates returns status 200 and no price. Rendering the page with the Crawling API's JS token, with dates in the URL, is what makes the price appear.

Do I need the normal token or the JS token for Airbnb?

The JS token. The normal token fetches static HTML, which on Airbnb is the same empty shell a plain fetch returns. The JS token renders the page in a real browser before handing back the HTML, so the listing fields, including the price, are present when BeautifulSoup parses them.

Why does the same listing show different prices?

Airbnb uses dynamic pricing. The nightly rate changes with the dates you request, weekends versus weekdays, demand, and season, and some hosts apply length-of-stay discounts. That is expected, not a bug in your scraper. If you want to compare prices, loop over several date ranges for the same room and record the rate for each, as the multi-date example in this guide does.

My selectors return None. What changed?

Almost certainly Airbnb's markup. Its hashed CSS class names change frequently, and section layouts shift over time, so selectors that worked last month can break. Anchor on the more stable data-section-id and data-testid attributes where you can, re-inspect a live page in your browser's dev tools when a field comes back empty, and update the selector. Periodic selector maintenance is normal for any production scraper.

How do I avoid getting blocked while scraping Airbnb?

Keep your per-IP request rate low, vary your targets instead of looping one listing across many dates back to back, and route through rotating residential IPs so no single address trips a rate limit. The Crawling API manages rotation and a trusted IP pool for you; if you build your own stack, that is the part to invest in. Watch the status codes and back off when you start seeing challenges.

What Airbnb data can I legally collect?

Stick to public listing data: the title, the nightly price, the public rating, and the location shown on the listing without logging in. Do not collect host or guest personal data, contact details, messages, or anything behind a login, and never bypass authentication. Respect Airbnb's terms of service and robots.txt, and for commercial or bulk use pursue an official partnership or API agreement rather than scaling a scraper against the public site.

Start Building

Crawl any site at scale, without fighting infrastructure.

Crawlbase handles proxies, fingerprints, and CAPTCHAs so your team ships data pipelines instead of maintaining crawl plumbing. 1,000 requests free, no card required.

Self-serve · No sales call required · Enterprise crawl volumes available