How to Scrape Airbnb Listings Without the API in 2026
Airbnb removed their public API in 2020. The only way to get pricing, availability, and listing data at scale is to scrape it directly.
Here's what actually works — and the pitfalls to avoid.
What You Can Extract
| Data Field | Accessible |
|---|---|
| Listing title, description | ✅ |
| Price per night, cleaning fee | ✅ |
| Ratings and review count | ✅ |
| Host info (name, superhost status) | ✅ |
| Amenities list | ✅ |
| Location (approximate) | ✅ |
| Photos | ✅ (URLs) |
| Availability calendar | ✅ |
| House rules | ✅ |
| Number of bedrooms/bathrooms | ✅ |
Airbnb's Anti-Scraping Setup
Airbnb uses a multi-layer approach:
- IP-based rate limiting — datacenter IPs get throttled within 20-30 requests
- Browser fingerprinting — checks Canvas, WebGL, audio fingerprint
- JavaScript rendering — most data loads after initial HTML
- Token-based API — internal API requires session tokens that rotate
The good news: the internal API is well-structured and returns clean JSON if you can get a valid token.
Method 1: Airbnb's Internal API (Most Efficient)
import requests
import json
from urllib.parse import urlencode
def search_airbnb_listings(location, checkin, checkout, adults=2, max_results=50):
"""
Search Airbnb listings for a location and date range.
Returns structured listing data.
"""
# Airbnb's internal search API
base_url = "https://www.airbnb.com/api/v3/StaysPdpSections"
# Build search parameters
params = {
"operationName": "StaysPdpSections",
"locale": "en",
"currency": "USD",
}
# GraphQL variables for the search
variables = {
"query": location,
"checkin": checkin, # "2026-05-01"
"checkout": checkout, # "2026-05-07"
"adults": adults,
"children": 0,
"infants": 0,
"pets": 0,
"refinementPaths": ["/homes"],
"tabId": "home_tab",
"itemsPerGrid": 20,
}
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/124.0.0.0",
"Accept": "application/json",
"X-Airbnb-API-Key": "d306zoyjsyarp7ifhu67rjxn52tv0t20", # Public app key
"Content-Type": "application/json",
}
# Alternative: use the explore API
explore_url = "https://www.airbnb.com/api/v3/ExploreSearch"
explore_params = {
"version": "1.8.9",
"satori_version": "1.1.0",
"query": location,
"checkin": checkin,
"checkout": checkout,
"adults": str(adults),
"currency": "USD",
"locale": "en",
"items_per_grid": "20",
}
r = requests.get(explore_url, params=explore_params, headers=headers, timeout=20)
if r.status_code != 200:
print(f"Error: {r.status_code}")
return []
data = r.json()
# Navigate the response structure
explore_tabs = data.get("explore_tabs", [])
listings = []
for tab in explore_tabs:
sections = tab.get("sections", [])
for section in sections:
items = section.get("listings", [])
for item in items:
listing = item.get("listing", {})
pricing = item.get("pricing_quote", {})
listings.append({
"id": listing.get("id"),
"name": listing.get("name"),
"city": listing.get("city"),
"room_type": listing.get("room_type_category"),
"bedrooms": listing.get("bedrooms"),
"bathrooms": listing.get("bathrooms"),
"beds": listing.get("beds"),
"person_capacity": listing.get("person_capacity"),
"price_per_night": pricing.get("rate", {}).get("amount"),
"currency": pricing.get("rate", {}).get("currency"),
"rating": listing.get("avg_rating"),
"review_count": listing.get("reviews_count"),
"superhost": listing.get("is_superhost"),
"url": f"https://www.airbnb.com/rooms/{listing.get('id')}",
})
return listings
# Example usage
listings = search_airbnb_listings(
location="Lisbon, Portugal",
checkin="2026-05-01",
checkout="2026-05-07",
adults=2
)
for listing in listings[:3]:
print(f"${listing['price_per_night']}/night | ⭐{listing['rating']} ({listing['review_count']} reviews)")
print(f" {listing['bedrooms']}br/{listing['bathrooms']}ba | {listing['name'][:60]}")
print()
Method 2: Playwright for Dynamic Content
For listing details pages (amenities, house rules, availability calendar):
from playwright.sync_api import sync_playwright
import json, re
def scrape_airbnb_listing(listing_id, checkin="2026-05-01", checkout="2026-05-07"):
"""Scrape full listing details from Airbnb listing page."""
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context(
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/124.0.0.0"
)
page = context.new_page()
url = f"https://www.airbnb.com/rooms/{listing_id}?check_in={checkin}&check_out={checkout}"
page.goto(url, wait_until="networkidle")
# Extract JSON-LD structured data (most reliable)
scripts = page.query_selector_all('script[type="application/json"]')
listing_data = {}
for script in scripts:
try:
data = json.loads(script.inner_text())
# Look for the main listing data
if "niobeMinimalClientData" in str(data):
listing_data = data
break
except:
continue
# Extract amenities from the page
amenity_elements = page.query_selector_all('[data-section-id="AMENITIES_DEFAULT"] li')
amenities = [el.inner_text() for el in amenity_elements]
# Extract price
price_el = page.query_selector('[data-testid="price-component"]')
price = price_el.inner_text() if price_el else None
browser.close()
return {
"id": listing_id,
"amenities": amenities,
"price_display": price,
"url": url,
"raw_data": listing_data,
}
listing = scrape_airbnb_listing("1234567", "2026-05-01", "2026-05-07")
print(f"Amenities: {listing['amenities'][:5]}")
Method 3: Apify Actor (No Session Management)
The Airbnb Listings Scraper handles proxy rotation, session tokens, and JavaScript rendering automatically.
import requests, time
run = requests.post(
"https://api.apify.com/v2/acts/lanky_quantifier~airbnb-listings-scraper/runs",
headers={"Authorization": "Bearer YOUR_APIFY_TOKEN"},
json={
"location": "Lisbon, Portugal",
"checkIn": "2026-05-01",
"checkOut": "2026-05-07",
"adults": 2,
"maxListings": 100,
"includeCalendar": True,
"includePricing": True
}
).json()["data"]
while True:
status = requests.get(
f"https://api.apify.com/v2/actor-runs/{run['id']}",
headers={"Authorization": "Bearer YOUR_APIFY_TOKEN"}
).json()["data"]["status"]
if status in ("SUCCEEDED", "FAILED"): break
time.sleep(5)
results = requests.get(
f"https://api.apify.com/v2/actor-runs/{run['id']}/dataset/items",
headers={"Authorization": "Bearer YOUR_APIFY_TOKEN"}
).json()
for listing in results[:3]:
print(f"${listing['price']}/night | ⭐{listing['rating']} | {listing['bedrooms']}br")
print(f" {listing['name'][:60]}")
Real-World Use Cases
Short-term rental investors: Track average nightly rates and occupancy patterns in target markets before buying property
Property managers: Monitor competitor listings in the same market, price competitively
Travel apps: Build location-specific pricing dashboards for budget planning
Research: Analyze Airbnb market penetration, host density, or average prices across cities
Arbitrage detection: Find markets where long-term rental costs are lower than Airbnb revenue potential
Anti-Detection Tips
import time, random
# Key rules:
# 1. Always use residential proxies - datacenter IPs get rate-limited within 20-30 requests
# 2. Add natural delays between requests
# 3. Use Chrome-like headers including cookie consent headers
def get_headers():
return {
"User-Agent": random.choice([
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/124.0.0.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/124.0.0.0",
]),
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Connection": "keep-alive",
}
def safe_delay():
time.sleep(random.uniform(2.0, 5.0))
Pricing
Using Apify: ~$2-3 per 1,000 listings scraped via pay-per-result pricing.
Running your own scraper: Residential proxy costs of ~$5-10/GB.
For periodic monitoring (weekly pricing updates for 50 markets): expect ~$5-10/month total.
Save hours on scraping setup: The $29 Apify Scrapers Bundle includes 35+ production-ready actors — Google SERP, LinkedIn, Amazon, TikTok, contact info, and more. Pre-configured inputs, working on day one.
Top comments (0)