kestrel actors › Hotel & flight prices
Airbnb Occupancy Rate Scraper — Calendar Availability & ADR
An airbnb occupancy rate per listing and month, read from the listing's public availability calendar, with a sampled nightly rate for each month — the two numbers behind every short term rental data subscription, as rows you own. Point it at a place, a pasted search URL or your own listing ids and every listing comes back as one row per calendar month: days still available, days blocked or booked, occupancy_pct, the typical minimum stay, the next free night, and rate_nightly from the first bookable stay of that month. It is an airbnb calendar scraper built for the question investors and hosts actually ask, and an airdna alternative priced per row rather than per market. No login, no API key, no browser.
Run Airbnb Occupancy Rate Scraper on Apify →
Airbnb occupancy rate pricing: what one row costs
| Event | What it is | Price | Charged for |
|---|---|---|---|
occupancy | Occupancy row (listing × month) | $0.002 | One calendar month of one listing: days available, days blocked or booked, occupancy %, minimum stay, and a sampled nightly rate for the month. Months dropped by minOccupancyPct, listing rows and status rows are free. |
Pay per event, one event per delivered occupancy row — one per listing and month. Listing rows, status rows, months dropped by minOccupancyPct, unknown places and listings whose calendar failed are free, and nothing a spending limit refused is ever delivered.
Some worked sizes at the listed per-row price:
- One listing, 12 months: 12 rows.
- A portfolio of 20 with their 40 competitors, 3 months, weekly: 180 rows a run, about 780 a month.
- A market of 100 listings, 6 months: 600 rows.
- A
minOccupancyPct: 80run that finds nothing: 0 rows, $0 beyond a fraction of a cent of compute.
Each listing costs one calendar request plus, with rates on, one request per month. The verified runs behind this listing used $0.0002–$0.004 of platform compute each. Exact per-row pricing is on this page's pricing tab.
Airbnb occupancy rate input reference
Every field of Airbnb Occupancy Rate Scraper — input, straight from the actor's input schema.
| Field | Type | Default | What it does |
|---|---|---|---|
locationQueries | array | — | Places to search exactly as you would type them on Airbnb: "Lisbon, Portugal", "Algarve", "Brooklyn, NY". Every listing found gets an occupancy row per month. An unrecognised place produces a status row (unknown_area) and costs nothing. |
startUrls | array | [] | Airbnb search URLs (their filters are honoured: room type, price band, bedrooms, map bounds) or listing URLs (https://www.airbnb.com/rooms/12345678). |
listingIds | array | [] | Numeric Airbnb listing ids — your own portfolio or a competitive set. No search needed; one occupancy row per id and month. |
maxListingsPerQuery | integer | 40 | Distinct listings to take per place or search URL (Airbnb returns at most about 280 for one search; split big areas into neighbourhoods for more). The main cost control for a market run. Range 1-280. |
months | integer | 3 | How many calendar months to read per listing, starting with the current month (1–12). One occupancy row per listing and month. Range 1-12. |
includeRates | boolean | true | Price the first bookable stay of each month (check-in on the first available day, the listing's minimum nights) and put its nightly rate on the occupancy row — an ADR proxy. One extra request per listing and month; turn off for a cheaper, availability-only run. |
adults | integer | 2 | Guests the sampled stay is priced for; Airbnb prices depend on guest count. Range 1-16. |
minOccupancyPct | number | 0 | 0 = deliver every month. N = deliver only months whose occupancy is at or above N %. Months under the line are dropped BEFORE billing, so a run looking for busy listings pays only for the busy months. Range 0-100. |
includeListingRow | boolean | false | Also emit one free listing row per listing (name, type, location, rating, reviews) — handy when you export the dataset to a sheet. |
currency | string | "USD" | Currency for the sampled rates (USD, EUR, GBP, …). Echoed on every occupancy row. |
locale | string | "en" | Airbnb locale for listing names (en, fr, de, es, pt, it, ja, …). |
proxyConfiguration | object | {"useApifyProxy": true} | Airbnb accepts Apify datacenter proxies for these calls; keep the default. |
sessions | integer | 4 | Concurrent egress IPs; each paced at perIp requests per second. Range 1-16. |
perIp | number | 1 | Pace per session. Airbnb tolerated 1 request/second and short bursts in tests. Range 0.1-3. |
Example input
{ "locationQueries": ["Lisbon, Portugal"], "maxListingsPerQuery": 100, "months": 6, "currency": "EUR" }
{ "listingIds": ["17088279", "34832567", "13055012"], "months": 3, "adults": 2, "includeListingRow": true }
Airbnb occupancy rate output fields
Row types in one dataset: occupancy (one per listing and calendar month — days available vs blocked or booked, occupancy %, minimum stay, a sampled nightly rate), listing (optional, free), status (per place, URL or listing id; never charged).
Dataset views in the Apify Console: Overview, Occupancy by month, Listings, Run status.
| Field | Type | What it holds |
|---|---|---|
type | string | Row type. |
id | string | Numeric Airbnb listing id. |
url | string | Listing URL. |
name | string | Listing name as Airbnb shows it. |
title | string | Search title, e.g. 'Apartment in Lisbon', or the listing page title. |
property_type | string | Apartment, House, Condo … (search rows). |
room_type | string | Entire home/apt, Private room, Shared room, Hotel room (listing page). |
location | string | Area from search or location text from the listing page. |
lat | number | |
lng | number | |
rating | number | |
reviews_count | integer | |
bedrooms | integer | |
beds | integer | |
bathrooms | number | |
person_capacity | integer | |
is_superhost | boolean | |
is_guest_favorite | boolean | |
query | string | The place search that produced the row; null for listing ids. |
month | string | Calendar month, YYYY-MM. |
days_in_month | integer | Calendar days in the month (28–31). |
days_total | integer | Days of the month counted: today onward. Past days are never counted, so the current month is partial. |
days_available | integer | Days of the month a guest could still book. |
days_unavailable | integer | Days not available — booked by a guest or blocked by the host; the calendar does not distinguish. |
occupancy_pct | number | days_unavailable / days_total × 100, one decimal. An occupancy proxy: blocked nights count as occupied. |
checkin_days | integer | Available days on which a stay may also start. |
nights_min | integer | The most common minimum stay across the month's days. |
next_available | string | First available day of the month, null when fully unavailable. |
first_day | string | First day counted for the month. |
last_day | string | Last day counted for the month. |
currency | string | Currency of the sampled rate. |
rate_status | string | ok = priced; no_stay = no bookable stay in the month; unavailable = the page refused the stay (usually adults exceeds the listing's capacity); unpriced = no price came back; error = the request failed; skipped = includeRates off. |
rate_check_in | string | Check-in of the sampled stay (first bookable stay of the month). |
rate_check_out | string | Check-out of the sampled stay. |
rate_nights | integer | Nights in the sampled stay — the listing's minimum stay, capped at 7. |
rate_total | number | Total price of the sampled stay for adults guests, as Airbnb displays it. |
rate_nightly | number | rate_total ÷ rate_nights — the month's nightly rate sample, an ADR proxy. |
listing_id | string | Status rows: the listing id the job was for. |
status | string | Status rows. |
resolved_place | string | Status rows: the place Airbnb actually searched (from its canonical URL). |
canonical_url | string | |
pages | integer | Status rows: search pages read. |
listings | integer | Status rows: listings whose calendar was read. |
months | integer | Status rows: occupancy rows delivered (and billed). |
filtered | integer | Status rows: months dropped by minOccupancyPct — never billed. |
rates | integer | Status rows: months whose rate sample was priced. |
duplicates | integer | Status rows: listings already handled by another job in this run. |
calendar_errors | integer | |
rate_errors | integer | |
detail_errors | integer | |
error | string | Status rows: the failure message, null when the job succeeded. |
fetched_at | string | UTC timestamp when the row was fetched. |
Example row from a real run
Taken from the actor's README — this is the shape your dataset holds.
{
"type": "occupancy", "id": "17088279", "url": "https://www.airbnb.com/rooms/17088279",
"name": "Bairro Alto Refuge", "title": "Apartment in Lisbon", "property_type": "Apartment", "room_type": "Entire home/apt",
"location": "Lisbon", "lat": 38.71209, "lng": -9.14346, "rating": 4.91, "reviews_count": 374,
"bedrooms": 1, "beds": 1, "bathrooms": 1, "person_capacity": 4, "is_superhost": false, "is_guest_favorite": true, "query": "Lisbon, Portugal",
"month": "2026-09", "days_in_month": 30, "days_total": 30, "days_available": 12, "days_unavailable": 18, "occupancy_pct": 60.0,
"checkin_days": 9, "nights_min": 3, "next_available": "2026-09-08", "first_day": "2026-09-01", "last_day": "2026-09-30",
"currency": "USD", "rate_status": "ok", "rate_check_in": "2026-09-08", "rate_check_out": "2026-09-11", "rate_nights": 3, "rate_total": 791, "rate_nightly": 263.67,
"fetched_at": "2026-08-29T08:40:12+00:00"
}
{ "month": "2026-10", "days_total": 31, "days_available": 0, "days_unavailable": 31, "occupancy_pct": 100.0, "next_available": null, "rate_status": "no_stay", "rate_nightly": null }
Airbnb occupancy rate in Python, JavaScript and curl
Python
from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("kestrel/airbnb-occupancy-scraper").call(run_input={
"locationQueries": ["Lisbon, Portugal"], "maxListingsPerQuery": 50, "months": 3, "currency": "EUR",
})
rows = [r for r in client.dataset(run["defaultDatasetId"]).iterate_items() if r["type"] == "occupancy"]
by_month = {}
for r in rows:
by_month.setdefault(r["month"], []).append(r["occupancy_pct"])
for month, pcts in sorted(by_month.items()):
print(month, round(sum(pcts) / len(pcts), 1), "% occupied across", len(pcts), "listings")
JavaScript (Node)
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('kestrel/airbnb-occupancy-scraper').call({
listingIds: ['17088279', '34832567'], months: 3, includeListingRow: true,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const r of items.filter((i) => i.type === 'occupancy')) console.log(r.name, r.month, r.occupancy_pct, r.rate_nightly, r.currency);
curl
curl -X POST "https://api.apify.com/v2/acts/kestrel~airbnb-occupancy-scraper/run-sync-get-dataset-items?token=<YOUR_APIFY_TOKEN>" \
-H 'Content-Type: application/json' \
-d '{"listingIds":["17088279"],"months":2,"currency":"USD"}'
The same three calls are runnable files in the examples repository: examples/airbnb-occupancy-scraper.py|.js|.sh.
Limits and honest notes
- Occupancy is a proxy. Blocked nights and booked nights look identical in the calendar. Every number here counts both as occupied; the README, the schema and the row descriptions say so, and the count of unavailable days sits next to the percentage for that reason.
- The rate is a sample, not an average. One stay per month — the first bookable one at the listing's minimum length, capped at seven nights, for
adultsguests. A month with weekend premiums or a stay priced mid-month can differ; runadultsandmonthsto match the stay you care about. - A search returns the site's ranking up to its cap (about 280 listings). Cover a city with neighbourhood queries or a pasted URL with map bounds.
- A calendar covers twelve months from the current month;
monthsis capped there. - Delisted or snoozed listings return a status row with an error and no charge.
- The query hashes the site uses change occasionally. The actor refreshes them from the site's bundles, a daily canary run watches it, and failures appear as
statusrows rather than silent zeros.
FAQ
Does it need an Airbnb API key, login or cookies?
No. There is no public airbnb availability api for guests, no key to request and no login involved. The actor uses the public key every visitor's browser uses and no session at all.
Is occupancy the same as bookings?
No — see the section above. The calendar shows availability; a night can be unavailable because a guest booked it or because the host blocked it. occupancy_pct counts both. For a market the difference is small and steady; for a single listing check days_unavailable and the host's pattern before treating it as revenue.
How do I get the airbnb occupancy rate by city?
Put the city in locationQueries with a maxListingsPerQuery large enough to be representative, choose months, and average occupancy_pct by month over the rows. For a neighbourhood, query it by name; the status row's resolved_place confirms what the site searched.
Where does the nightly rate come from if the calendar has no prices?
From the listing page, priced for a real stay: the first bookable check-in of the month with the listing's minimum nights, for the adults you set. rate_check_in, rate_check_out and rate_nights are on the row so you can reproduce the quote; rate_status explains any missing number.
Can I track airbnb booked dates over time?
Not the dates of individual bookings — the calendar only says whether a night is free. What you can track is the change: schedule the run and compare days_available for the same listing and month across fetched_at. Nights that went from available to unavailable between runs are the booking pace.
Can I get airbnb nightly rate history?
Not retroactively — the site does not publish it. You can build it: a scheduled run appends a dated rate_nightly per listing and month every time it runs.
How is this different from the Airbnb Scraper?
The Airbnb Scraper prices a specific stay for every listing and can attach a full day-by-day calendar and reviews; it bills per listing, per calendar month and per review. This one skips the stay price, reads the calendar for everyone, folds each month into one occupancy row with a rate sample, and bills only those rows — the cheaper shape for market and portfolio occupancy work. Use both when you want the stay price and the occupancy together.
Which currencies and languages?
Any ISO currency for the rate sample and any site locale for names. currency is echoed on every row so a mixed export never guesses.
Is there a free tier?
Listing rows, status rows, filtered months, unknown places and failed calendars are always free, and Apify's free plan includes monthly platform credit you can spend on the paid rows.
How to use Airbnb Occupancy Rate Scraper
Related scrapers
Run Airbnb Occupancy Rate Scraper
The actor lives on the Apify Store, where the pricing tab carries the live per-row price and the input form has every field above: apify.com/kestrel/airbnb-occupancy-scraper