Airbnb Listings Scraper (search + details + reviews)
Pricing
from $3.00 / 1,000 listing scrapeds
Airbnb Listings Scraper (search + details + reviews)
Airbnb search results and full listing details for any location: price, rating, host, amenities, coordinates, reviews. Beats the ~270-result cap with automatic price-band splitting. Plain HTTP, pay per listing.
Pricing
from $3.00 / 1,000 listing scrapeds
Rating
0.0
(0)
Developer
Swaraj Samant Singh
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Share
Get every Airbnb listing for a location or a search URL as clean JSON/CSV: price per night, total for your dates, rating, review count, host, coordinates, bedrooms/beds/baths, photos. Turn on details for amenities, description, rating breakdown, house rules and cancellation policy, and reviews for the newest guest reviews. Pay per listing.
Data source: airbnb.com, the same GraphQL endpoints the Airbnb web app calls. No browser, no login. Airbnb caps any single search at about 270 results; when you ask for more the actor automatically re-runs the search per price band until it has what you asked for (see Limitations).
What you get per listing
Search-level fields (always, one request per 18 listings):
{"listingId": "15971352","url": "https://www.airbnb.com/rooms/15971352","title": "Modern Downtown Castle View Apartment","roomType": "Entire home/apt","propertyType": "Apartment","area": "Arroios","lat": 38.71491,"lng": -9.13652,"pricePerNight": 184.23,"totalPrice": 716,"originalTotalPrice": 922,"nights": 5,"currency": "USD","checkIn": "2026-08-24","checkOut": "2026-08-29","rating": 4.88,"reviewsCount": 432,"isSuperhost": true,"badges": ["Superhost"],"bedrooms": 1,"beds": 1,"bathrooms": 1,"images": ["https://a0.muscache.com/im/pictures/094c8a4a-....jpg", "..."],"query": "Lisbon, Portugal","scrapedAt": "2026-08-17T19:41:23.209Z"}
With includeDetails (one more request per listing) the same row also carries:
{"propertyType": "Entire apartment","propertyTypeCode": "CONDOMINIUM","city": "Lisbon","isExactLocation": false,"maxGuests": 2,"ratingCleanliness": 4.9, "ratingAccuracy": 4.91, "ratingCheckin": 4.87,"ratingCommunication": 4.97, "ratingLocation": 4.74, "ratingValue": 4.79,"ratingDistribution": { "5": 89.4, "4": 9.3, "3": 1.4, "2": 0, "1": 0 },"isGuestFavorite": true,"hostId": "103698670","hostName": "Pedro","hostIsVerified": true,"hostRating": 4.87,"hostReviewsCount": 1577,"hostYears": 9,"hostResponseRate": 100,"hostResponseTime": "within an hour","hostAbout": "Lisboner, citizen of the world! ...","cohosts": ["Mila", "Tetiana"],"description": "This apartment is located in the Martim Moniz Square...","highlights": ["Furry friends welcome", "24-hour self check-in"],"amenities": ["City skyline view", "Wifi", "Kitchen", "Washing machine", "..."],"amenitiesUnavailable": ["Exterior security cameras on property"],"houseRules": ["Check-in after 3:00 pm", "Checkout before 11:00 am", "No smoking"],"safetyNotes": ["Carbon monoxide alarm installed", "Smoke alarm installed"],"cancellationPolicy": "Moderate","images": ["... up to 20 photos ..."]}
With includeReviews, reviews is an array of { id, date, rating, text, language, reviewerName, reviewerId, hostResponse }, newest first.
| Field | Present | Meaning |
|---|---|---|
listingId, url | always | Airbnb's numeric listing ID and its canonical airbnb.com/rooms/<id> URL. Rows are de-duplicated on listingId |
title | always | Listing name as the host wrote it |
roomType | always | Entire home/apt, Private room, Shared room, Hotel room. From the search card it is inferred from Airbnb's label; with details it is Airbnb's exact spaceType |
propertyType, area | always | Airbnb's card label split: "Apartment in Arroios" gives Apartment + Arroios. area is whatever Airbnb prints there (city, town or neighbourhood). Details add city and propertyTypeCode |
lat, lng | always | Coordinates from Airbnb. isExactLocation (details) tells you whether Airbnb shows the exact spot or an approximate circle |
pricePerNight, totalPrice, nights, checkIn, checkOut | when Airbnb prices the listing | Numbers in currency. totalPrice is Airbnb's total for the stay (its "total before taxes" display); pricePerNight is the nightly rate from Airbnb's own price breakdown. If you gave checkIn/checkOut, they are for your dates. If not, Airbnb picks example dates per listing and reports them in checkIn/checkOut; the price is for those |
originalTotalPrice | when discounted | Pre-discount total |
rating, reviewsCount | when the listing has reviews | Average and count. New listings carry isNew: true instead |
isSuperhost, isGuestFavorite, badges | when true / present | From the search card badges; details confirm isSuperhost from the host profile |
bedrooms, beds, bathrooms | when shown | Studio = bedrooms: 0 |
images | always | Up to 6 photo URLs from the search card, up to 20 with details |
hostId, hostName, hostIsVerified, hostRating, hostReviewsCount, hostYears, hostResponseRate, hostResponseTime, hostAbout, cohosts | details | Host profile |
ratingCleanliness ... ratingValue, ratingDistribution | details, when rated | Airbnb's six category ratings and the star distribution in percent |
description, highlights, amenities, amenitiesUnavailable, houseRules, safetyNotes, cancellationPolicy, maxGuests | details | Description is plain text (HTML stripped). amenitiesUnavailable is Airbnb's "not included" list (no smoke alarm etc.) |
reviews | with includeReviews | Newest first, up to maxReviewsPerListing |
query, scrapedAt | always | Which input entry produced the row, and when |
Fields Airbnb does not provide for a listing are omitted, never filled with nulls or guesses.
Input
{"locationQueries": ["Lisbon, Portugal", "Goa, India"],"startUrls": ["https://www.airbnb.com/s/Barcelona--Spain/homes?room_types%5B%5D=Entire%20home%2Fapt&price_max=150"],"checkIn": "2026-10-10","checkOut": "2026-10-14","adults": 2,"currency": "USD","priceMin": 50,"priceMax": 300,"minBedrooms": 1,"roomTypes": ["Entire home/apt"],"maxListingsPerQuery": 500,"includeDetails": true,"includeReviews": false,"maxReviewsPerListing": 20}
| Field | Default | Notes |
|---|---|---|
locationQueries | [] | Places as you would type them into Airbnb. One search each |
startUrls | [] | Airbnb search URLs copied from the browser. Every query parameter is passed through unchanged (map bounds ne_lat/sw_lng, amenities[], guest_favorite, flexible_trip_lengths, monthly_start_date...). URL parameters override the fields below |
checkIn, checkOut | none | YYYY-MM-DD. Give both for exact prices for that stay. Without dates each listing is priced for example dates Airbnb picks (reported per row) |
adults | 2 | |
currency | USD | ISO code. Prices, priceMin/priceMax and price bands are in this currency |
priceMin, priceMax | none | Per night |
minBedrooms | none | |
roomTypes | all | Any of Entire home/apt, Private room, Shared room, Hotel room |
maxListingsPerQuery | 100 | Above ~270 the actor splits the search into price bands automatically (see below) |
maxItems | none | Hard cap across all searches |
includeDetails | false | Fetches airbnb.com/rooms/<id> for each listing. Roughly doubles the run time |
includeReviews, maxReviewsPerListing | false, 20 | 24 reviews per request, newest first |
minDelayMs | 1500 | Pause between requests. Measured 2026-08-18: 0.8-1.5 s pacing ran 65+ requests from one IP with no block, laptop and datacenter VM alike |
proxyConfiguration | Apify residential | Residential is the safe default for Airbnb; datacenter worked in tests but Airbnb is known to block datacenter ranges. Sessions rotate on 403/429 |
At least one of locationQueries or startUrls is required.
Pricing
Pay per event:
| Event | Price |
|---|---|
| Actor start | $0.005 per run |
| Listing scraped | $0.003 per listing row |
| Listing details | $0.005 per listing, only with includeDetails |
| Review scraped | $0.0005 per review, only with includeReviews |
1,000 listings without details = about $3. With details = about $8. Set "Maximum total charge" on the run to cap spend; the actor stops cleanly at the cap and everything already scraped stays in the dataset.
Getting past Airbnb's ~270 result cap
Airbnb serves at most 15 pages of 18 results for any one search, whatever the real count ("1,000+ places"). Other scrapers stop there. This actor reads the total from the first page; if the search is saturated and you asked for more, it drops the search and re-runs it as two per-night price bands (Airbnb's slider range halved), then keeps halving whichever band is still saturated. Each split costs one extra request. Rows are de-duplicated on listingId across bands, so a listing that sits on a band edge appears once. Every band split is logged:
"Lisbon, Portugal": Airbnb caps this search at ~270 of 1000+ listings; splitting into price bands 0-1025, 1026-2000, 2001-∞"Lisbon, Portugal" [0-1025 USD/night]: Airbnb caps this search at ~270 of 1000+ listings; splitting into price bands 0-537, 538-1025...
Measured 2026-08-18: 350 unique Lisbon listings took 33 requests (42 s); 30 listings with details took 34 requests (56 s).
For very dense areas (all of Lisbon at 1,000+ listings under $60/night) a band can be one currency unit wide and still saturated; that band yields its 270 and the actor moves on. Add a startUrls entry with map bounds (ne_lat, ne_lng, sw_lat, sw_lng) or roomTypes to slice further.
Limitations
- Prices are Airbnb's display prices:
totalPriceis the stay total as Airbnb shows it in search (typically before taxes),pricePerNightis the nightly rate from Airbnb's own breakdown. Cleaning and service fees are not itemised. WithoutcheckIn/checkOutthe price is for Airbnb-chosen example dates, which differ per listing; give dates for comparable numbers. roomTypefrom search cards is inferred from Airbnb's label ("Room in Lisbon" = private room, otherwise entire home). Turn onincludeDetailsfor Airbnb's exact value.- Availability calendar and per-date pricing tables are not scraped.
- Airbnb's persisted-query hashes change with its builds. The actor ships the current ones and, when Airbnb answers
persisted_query_not_found, rescans Airbnb's JS bundles for the new hash at run time. If Airbnb restructures the payloads themselves the actor fails loudly rather than returning wrong data. - Geo redirects: Airbnb sends non-US IPs to a local domain (
airbnb.co.in,airbnb.co.uk...). The actor follows that once and uses it; data and IDs are identical,urlalways points atairbnb.com. - Rate limits: Airbnb answers abuse with 403 or a 429. The actor paces requests (
minDelayMs), backs off 5 s / 15 s / 45 s / 135 s and rotates to a new proxy session on each block. Runs of 350 listings from a single IP at 1 s pacing were not blocked in testing.
FAQ
Why fewer rows than Airbnb's "1,000+ places"? maxListingsPerQuery caps each search (default 100). Raise it and the actor splits by price band. Duplicates across bands and across your queries are removed.
Do I get all listings on the map? You get what Airbnb's search returns for that query and filters, which is what a user sees. For a strict area use a startUrls entry with the map bounds Airbnb puts in the URL when you drag the map.
How do I get exact prices for my dates? Set checkIn/checkOut/adults. nights, totalPrice, pricePerNight then refer to that stay.
Are the listing IDs and URLs real? Yes: listingId is decoded from Airbnb's DemandStayListing:<id> global ID and url is https://www.airbnb.com/rooms/<id>, which opens the listing.
Does it use a browser? No, plain HTTP against Airbnb's own API endpoints, so it runs on 256 MB and a search page of 18 listings is one request.
Which host, and what happened, at the end of a run? The run log ends with a Done. line and the key-value store holds RUN_STATS (listings, searches, priceBandSearches, failedSearches, details, detailFailures, reviews, reviewFailures, httpRequests, host).
Local development
npm install# put your input in storage/key_value_stores/default/INPUT.jsonapify run --purge # or: npm startnpm test # offline parser checks on saved Airbnb payloads + one live search (skips when blocked)ACTOR_TEST_PAY_PER_EVENT=1 ACTOR_MAX_TOTAL_CHARGE_USD=6 npm start # exercise the pay-per-event cutoff locally