Airbnb Availability Calendar Scraper - Prices, Listings
Pricing
$2.90 / 1,000 listings
Airbnb Availability Calendar Scraper - Prices, Listings
Airbnb scraper past the 270-row search ceiling: 1916 unique listings where flat pagination returns 257, measured in 117 seconds. The paginated reviews route returns all 105 reviews of a listing where the older route returns 7. 12-month availability calendar. Airbnb occupancy rate data.
Pricing
$2.90 / 1,000 listings
Rating
0.0
(0)
Developer
Snow Leo Data
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
4 days ago
Last modified
Categories
Share
Airbnb Scraper — listings, prices, availability calendar and reviews
Airbnb's own search hands out at most 270 rows per query: the response carries exactly 15 page cursors of 18 rows, and those 270 rows contain 227 distinct listings, because the same card is repeated across pages. That is the ceiling every Airbnb scraper hits, and the largest competitor says so in its own README: "up to 240 results for one search query".
This Actor goes past it with an adaptive map grid. Measured on 2026-09-13,
query Paris, France, this Actor's own crawler:
| Mode | Queries | Unique listings |
|---|---|---|
Flat pagination (gridSplit: off) | 7 | 257 |
Adaptive map grid (gridSplit: auto) | 63 | 1 916 |
7.46x more listings in 117 seconds.
Everything else rides in the same row: listing page details, the day-by-day availability calendar with occupancy rates, and every review. One dataset row per listing, whether that row carries 0 reviews or 200.
What you get
A single flat row per listing. A full run produced 108 top-level columns and 164 leaf paths (arrays expanded one level) on 2026-09-13. The dataset schema declares 133 fields in total, because some of them only exist when the source publishes them.
Core identity and location: listing_id, url, android_url, ios_url,
name, title, seo_title, meta_description, latitude, longitude,
location_subtitle, location_is_exact, location_descriptions.
Property shape: room_type ("Entire home/apt", "Private room"),
property_type, space_type, home_tier, person_capacity,
max_guest_capacity, bedrooms, beds, baths, overview_title,
overview_items, sleeping_arrangements, is_new_listing, is_luxe,
is_hotel_property, structured_content.
Price: price_label, price_amount, price_per_night, price_currency,
price_nights, price_qualifier, price_original_label,
price_original_amount, price_is_discounted, price_breakdown,
available_dates_label. Set checkIn and checkOut and the price becomes the
real total for those dates instead of an estimate.
Quality: rating, reviews_count, rating_label, rating_accuracy,
rating_checkin, rating_cleanliness, rating_communication,
rating_location, rating_value, badges, is_guest_favorite.
Amenities and rules: amenities (grouped, with icon and availability flag),
amenity_names, amenities_count, amenities_unavailable, house_rules,
house_rules_additional, check_in_after, check_out_before,
self_check_in, max_guests_rule, smoking_allowed, parties_allowed,
pets_allowed, pets_allowed_rule, max_pet_count, safety_items,
accessibility_features, highlights, cancellation_policies.
Host: host_id, host_name, host_title, host_is_superhost,
host_is_verified, host_photo, host_rating, host_reviews_count,
host_rating_count, host_years_hosting, host_months_hosting,
host_highlights, host_about, host_response_rate, host_response_time,
host_is_airbnb_managed, host_can_be_messaged, cohosts.
Text and photos: description_text, description_html,
description_sections, description_language, images (with Airbnb's own
photo tags such as Living_room), images_count, search_images, thumbnail.
Availability: calendar_days (date, available, bookable, check-in allowed,
check-out allowed, minimum and maximum nights), calendar_days_scanned,
calendar_first_date, calendar_last_date, available_days_30,
available_days_60, available_days_90, available_days_365,
occupancy_rate_30, occupancy_rate_60, occupancy_rate_90,
occupancy_rate_365, min_nights_median, next_available_date,
next_blocked_date.
Reviews: reviews (id, rating, full text, date, language, reviewer name, photo,
profile link, tenure, host response), reviews_total, reviews_returned,
reviews_average_returned, review_languages, review_topics,
latest_review_at.
Run bookkeeping: change_type in incremental mode, locale, currency,
scraped_at.
How the ceiling is broken
The search endpoint accepts a map rectangle (ne_lat, ne_lng, sw_lat,
sw_lng) instead of a text query, and each rectangle gets its own 270-row
budget. So the Actor takes the rectangle Airbnb itself reports for your
location, splits it, and walks the cells.
The split is adaptive rather than fixed. A cell is divided again only when it filled its own budget and brought listings that were not seen before. Splitting an empty cell would spend queries on nothing, and splitting a cell whose listings were all duplicates would spend them on rows you already have. Cells go three levels deep at most.
The rectangle itself is not guessed. Airbnb returns a mapBoundsHint with the
answer, but that hint is narrower than the real city — for Paris it gives
48.827–48.901 while the listings actually run 48.806–48.904. So the Actor takes
the union of the hint and the bounding box of the listings that came back, and
pads it. The bounds used are written into REPORT.grid_bounds so you can check
them.
Duplicates are removed by listing id before anything is written, and the count
of repeats the source sent is reported as duplicates_from_source. On the Paris
measurement the source sent 571 repeats inside 1 916 unique listings.
You can turn the grid off (gridSplit: off) when you only need a quick sample,
or force a side (2 to 6) when you know the area.
Reviews without the 7-review trap
The old public endpoint /api/v2/homes_pdp_reviews still answers HTTP 200 and
looks like a full review feed. It is not. Measured on listing
1431944184823200762, which has 105 reviews: it returns 7 reviews and
ignores _offset entirely — offset 0 and offset 7 return the same seven rows.
A scraper built on it silently caps every listing at seven reviews and reports
success.
This Actor uses the paged reviews query that the listing page itself uses:
24 rows on the first page, 50 on the next, zero overlap between them, all 105
reachable. It also returns review_topics, Airbnb's own topic tags with
counts (Location 58, Hospitality 58, Cleanliness 37 on that listing),
which no competitor in the niche exposes.
Reviews live inside the listing row. Fifty reviews cost exactly as much as zero, because you are charged per listing.
Availability and occupancy
includeCalendar adds the day-level calendar, up to 12 months — 12 is the
source's own cap per request, read out of its JavaScript. Each day carries
available, bookable, available_for_checkin, available_for_checkout,
min_nights and max_nights.
From those days the Actor computes occupancy rates for 30, 60, 90 and 365 days. A window is only reported when the calendar actually covers it: asking for 3 months will not produce a "365-day occupancy" figure computed from 91 days, because such a number looks annual and is not.
This is what makes the Actor usable for short-term-rental market research:
occupancy_rate_90 plus price_per_night over a whole city is a revenue
estimate, and it comes from the source rather than from a model.
Filters, and where each one runs
Filters that Airbnb itself understands are sent to Airbnb, so non-matching
listings are never collected and never charged: priceMin, priceMax,
minBedrooms, minBeds, minBathrooms, roomTypes, propertyTypes,
amenities (36 ids), hostLanguages (70 languages), guestFavoriteOnly,
instantBookOnly, checkIn/checkOut and the guest counts. The amenity and
language lists were read out of Airbnb's own filter panel rather than typed by
hand, so the ids match the site exactly.
Filters Airbnb does not accept run here, after collection: minRating,
minReviewsCount, minPricePerNight, maxPricePerNight, nameContains,
excludeKeywords, requiredAmenities, superhostOnly, maxOccupancy30,
minAvailableDays30.
Two rules govern them, and both exist because breaking them has cost real buyers real money:
A missing field is not a mismatch. A listing with no rating yet passes
minRating. Measured: with minPricePerNight: 900000 on Lisbon, 34 of 37
listings were dropped and 3 survived — the three that publish no nightly price.
Dropping them would have been a silent lie about the data.
maxItems counts delivered rows, not scanned ones. Filters run before the
count, so asking for 20 listings with a filter returns 20 listings, not an empty
dataset.
Filters that depend on the listing page or the calendar are applied in two stages: whatever the search result can already decide is decided before the listing page is fetched, so the Actor does not pay for a page it is about to throw away.
Monitoring mode
onlyNew makes the Actor remember what earlier runs delivered, in a named key
value store that survives between runs. The next run writes only listings that
are new or whose price, nightly price, rating, review count, occupancy or
availability changed, and stamps each row with change_type
(NEW / UPDATED / UNCHANGED).
The description is deliberately not part of the change fingerprint: the source edits invisible details in it, and every listing would look "updated" every day.
A monitoring run that finds nothing new is supposed to return zero rows — that is the point, you do not pay twice for the same row. So the Actor names the reason instead of leaving a silent empty dataset.
Named reasons for an empty dataset
A green run with an empty dataset is the worst kind of failure: the buyer pays
for the start, receives nothing and cannot tell why. REPORT.empty_reason is
always set when the dataset is empty, to one of six values:
contradictory-input— the input could not return rows whatever the source answered. Detected before the first network request, so nothing is downloaded and nothing beyond the run start is charged. Example:superhostOnlywithincludeDetails: false— the superhost flag lives on the listing page, so every row would be dropped for a field that was never fetched. The exact sentences are inREPORT.input_problems.charge-limit-reached— your spending limit for the run was already used up.filtered-out— listings were collected and your filters removed all of them.REPORT.filtered_outsays which filter removed how many.nothing-changed— incremental mode, everything had already been delivered.source-returned-nothing— Airbnb returned no listings for this search.nothing-collected— listings were seen but none survived to the dataset.
Reliability
Airbnb's GraphQL operations are addressed by a signature that changes with every
frontend release. Hardcoding one turns a scraper into a time bomb. This Actor
pins the current signatures, and when Airbnb rejects one it reads the fresh
signature out of the site's own JavaScript bundles and retries once. Proved by
setting the search signature to 64 zeros: the run recovered in 35.7 seconds and
REPORT.operation_hashes showed bundle instead of pinned.
Rows are written to the dataset before they are marked as delivered, so an interrupted run never loses rows; the leftovers arrive on the next run.
Input examples
Whole city, everything on:
{"locationQueries": ["Austin, Texas"],"maxItems": 1000,"includeDetails": true,"includeCalendar": true,"calendarMonths": 12,"includeReviews": true,"maxReviews": 50}
Daily price monitor for a set of listings:
{"startUrls": ["https://www.airbnb.com/rooms/1431944184823200762","https://www.airbnb.com/rooms/20669368"],"includeCalendar": true,"calendarMonths": 3,"onlyNew": true}
Market scan with source-side filters only:
{"locationQueries": ["Lisbon, Portugal"],"maxItems": 500,"roomTypes": ["Entire home/apt"],"minBedrooms": 2,"amenities": ["4", "5", "7"],"priceMax": 250,"includeDetails": false,"compactOutput": true}
FAQ
How many listings can I get from one city?
More than the 270 Airbnb's own search will show you. Measured on Paris: 1 916
unique listings in 63 queries and 117 seconds. Bigger orders keep splitting the
map until either the order is filled or the query budget runs out; the budget
and how it was spent are in REPORT.
Do reviews cost extra? No. Reviews are part of the listing row, so a listing with 200 reviews is one dataset row, same as a listing with none.
Why did I get fewer rows than maxItems?
Either the area genuinely has fewer listings, or your filters removed them.
REPORT.filtered_out names the filter and the count; REPORT.targets gives the
unique listings collected per target and the queries spent.
Do I need proxies? No. The Actor uses no proxies and no browser. It was verified from Apify's own cloud IPs before a line of it was written, because a source that answers from a laptop does not always answer from a datacenter.
Which currency are prices in?
The one you pick in currency (30 supported). Without it, prices come back in
the currency Airbnb picks for the run's IP address, which makes rows from
different runs incomparable.
Can I scrape a single listing I already have the URL for?
Yes. Put it in startUrls, or put the numeric id in listingIds.
Does it work for hotels on Airbnb?
Yes. Hotel rooms are returned with is_hotel_property: true; some of them
publish no nightly price, and those rows carry price_per_night: null rather
than a guess.
How fresh is the availability calendar?
It is read live at run time. calendar_first_date and calendar_last_date show
the exact window, and scraped_at timestamps the row.
Limitations, named honestly
No separate service fee or "total before taxes". The largest competitor
returns price.breakDown.serviceFee and price.breakDown.totalBeforeTaxes.
Measured 2026-09-13 with check-in and check-out set: the search response now
carries only the "N nights x rate" line plus any resort fee, and the listing page
returns structuredDisplayPrice: null until a separate booking query runs in the
browser. Airbnb moved to all-in nightly display pricing. This Actor reports what
the source returns and does not invent the split.
Prices for direct listing URLs are the listing-page price. When you pass a
room URL or a listing id without a location search, the dated price breakdown
that the search response carries is not available, because that breakdown only
exists in search results. Pass a location query with checkIn/checkOut if you
need dated totals.
Exact coordinates are approximate before booking. Airbnb publishes an offset
location for most listings; location_is_exact tells you which ones are exact.
A made-up place name does not fail — it fuzzy-matches. Measured: searching
Zzqqxxnotaplaceatall, Nowhere returns a full page of 40 real listings rather
than an error. Check location_subtitle on the rows if you are unsure whether
the spelling was understood.
Search results are not stable between runs. Airbnb reshuffles the order, so
two runs with a small maxItems will not return the same listings. Use
listingIds or startUrls when you need a fixed set.
The 365-day occupancy needs a 12-month calendar. With calendarMonths: 3
the 365-day figures are simply absent rather than extrapolated.
Review text is returned in the language it was written in, plus Airbnb's
translation when the source provides one. review_languages lists what came
back.
What else is this called?
Names buyers use: Airbnb scraper, Airbnb availability calendar, Airbnb occupancy data, short-term rental occupancy, STR data, Airbnb reviews scraper. The calendar covers twelve months ahead, and the occupancy shares are computed from it.
Other names for the fields: cleaning fee and price breakdown (price_breakdown), occupancy rate for a short-term rental (occupancy_rate_30, occupancy_rate_90), superhost dataset (host_is_superhost), pet friendly rentals data (pets_allowed), nightly rate history (price_per_night). Those occupancy and price fields are the raw data people use for rental arbitrage research; the Actor returns the numbers, it does not do the analysis.
