Airbnb Listings Scraper (search + details + reviews) avatar

Airbnb Listings Scraper (search + details + reviews)

Pricing

from $3.00 / 1,000 listing scrapeds

Go to Apify Store
Airbnb Listings Scraper (search + details + reviews)

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

Swaraj Samant Singh

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

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.

FieldPresentMeaning
listingId, urlalwaysAirbnb's numeric listing ID and its canonical airbnb.com/rooms/<id> URL. Rows are de-duplicated on listingId
titlealwaysListing name as the host wrote it
roomTypealwaysEntire 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, areaalwaysAirbnb'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, lngalwaysCoordinates from Airbnb. isExactLocation (details) tells you whether Airbnb shows the exact spot or an approximate circle
pricePerNight, totalPrice, nights, checkIn, checkOutwhen Airbnb prices the listingNumbers 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
originalTotalPricewhen discountedPre-discount total
rating, reviewsCountwhen the listing has reviewsAverage and count. New listings carry isNew: true instead
isSuperhost, isGuestFavorite, badgeswhen true / presentFrom the search card badges; details confirm isSuperhost from the host profile
bedrooms, beds, bathroomswhen shownStudio = bedrooms: 0
imagesalwaysUp to 6 photo URLs from the search card, up to 20 with details
hostId, hostName, hostIsVerified, hostRating, hostReviewsCount, hostYears, hostResponseRate, hostResponseTime, hostAbout, cohostsdetailsHost profile
ratingCleanliness ... ratingValue, ratingDistributiondetails, when ratedAirbnb's six category ratings and the star distribution in percent
description, highlights, amenities, amenitiesUnavailable, houseRules, safetyNotes, cancellationPolicy, maxGuestsdetailsDescription is plain text (HTML stripped). amenitiesUnavailable is Airbnb's "not included" list (no smoke alarm etc.)
reviewswith includeReviewsNewest first, up to maxReviewsPerListing
query, scrapedAtalwaysWhich 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
}
FieldDefaultNotes
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, checkOutnoneYYYY-MM-DD. Give both for exact prices for that stay. Without dates each listing is priced for example dates Airbnb picks (reported per row)
adults2
currencyUSDISO code. Prices, priceMin/priceMax and price bands are in this currency
priceMin, priceMaxnonePer night
minBedroomsnone
roomTypesallAny of Entire home/apt, Private room, Shared room, Hotel room
maxListingsPerQuery100Above ~270 the actor splits the search into price bands automatically (see below)
maxItemsnoneHard cap across all searches
includeDetailsfalseFetches airbnb.com/rooms/<id> for each listing. Roughly doubles the run time
includeReviews, maxReviewsPerListingfalse, 2024 reviews per request, newest first
minDelayMs1500Pause 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
proxyConfigurationApify residentialResidential 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:

EventPrice
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: totalPrice is the stay total as Airbnb shows it in search (typically before taxes), pricePerNight is the nightly rate from Airbnb's own breakdown. Cleaning and service fees are not itemised. Without checkIn/checkOut the price is for Airbnb-chosen example dates, which differ per listing; give dates for comparable numbers.
  • roomType from search cards is inferred from Airbnb's label ("Room in Lisbon" = private room, otherwise entire home). Turn on includeDetails for 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, url always points at airbnb.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.json
apify run --purge # or: npm start
npm 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