kestrel actors docs

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

EventWhat it isPriceCharged for
occupancyOccupancy row (listing × month)$0.002One 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:

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.

FieldTypeDefaultWhat it does
locationQueriesarrayPlaces 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.
startUrlsarray[]Airbnb search URLs (their filters are honoured: room type, price band, bedrooms, map bounds) or listing URLs (https://www.airbnb.com/rooms/12345678).
listingIdsarray[]Numeric Airbnb listing ids — your own portfolio or a competitive set. No search needed; one occupancy row per id and month.
maxListingsPerQueryinteger40Distinct 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.
monthsinteger3How many calendar months to read per listing, starting with the current month (1–12). One occupancy row per listing and month. Range 1-12.
includeRatesbooleantruePrice 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.
adultsinteger2Guests the sampled stay is priced for; Airbnb prices depend on guest count. Range 1-16.
minOccupancyPctnumber00 = 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.
includeListingRowbooleanfalseAlso emit one free listing row per listing (name, type, location, rating, reviews) — handy when you export the dataset to a sheet.
currencystring"USD"Currency for the sampled rates (USD, EUR, GBP, …). Echoed on every occupancy row.
localestring"en"Airbnb locale for listing names (en, fr, de, es, pt, it, ja, …).
proxyConfigurationobject{"useApifyProxy": true}Airbnb accepts Apify datacenter proxies for these calls; keep the default.
sessionsinteger4Concurrent egress IPs; each paced at perIp requests per second. Range 1-16.
perIpnumber1Pace 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.

FieldTypeWhat it holds
typestringRow type.
idstringNumeric Airbnb listing id.
urlstringListing URL.
namestringListing name as Airbnb shows it.
titlestringSearch title, e.g. 'Apartment in Lisbon', or the listing page title.
property_typestringApartment, House, Condo … (search rows).
room_typestringEntire home/apt, Private room, Shared room, Hotel room (listing page).
locationstringArea from search or location text from the listing page.
latnumber
lngnumber
ratingnumber
reviews_countinteger
bedroomsinteger
bedsinteger
bathroomsnumber
person_capacityinteger
is_superhostboolean
is_guest_favoriteboolean
querystringThe place search that produced the row; null for listing ids.
monthstringCalendar month, YYYY-MM.
days_in_monthintegerCalendar days in the month (28–31).
days_totalintegerDays of the month counted: today onward. Past days are never counted, so the current month is partial.
days_availableintegerDays of the month a guest could still book.
days_unavailableintegerDays not available — booked by a guest or blocked by the host; the calendar does not distinguish.
occupancy_pctnumberdays_unavailable / days_total × 100, one decimal. An occupancy proxy: blocked nights count as occupied.
checkin_daysintegerAvailable days on which a stay may also start.
nights_minintegerThe most common minimum stay across the month's days.
next_availablestringFirst available day of the month, null when fully unavailable.
first_daystringFirst day counted for the month.
last_daystringLast day counted for the month.
currencystringCurrency of the sampled rate.
rate_statusstringok = 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_instringCheck-in of the sampled stay (first bookable stay of the month).
rate_check_outstringCheck-out of the sampled stay.
rate_nightsintegerNights in the sampled stay — the listing's minimum stay, capped at 7.
rate_totalnumberTotal price of the sampled stay for adults guests, as Airbnb displays it.
rate_nightlynumberrate_total ÷ rate_nights — the month's nightly rate sample, an ADR proxy.
listing_idstringStatus rows: the listing id the job was for.
statusstringStatus rows.
resolved_placestringStatus rows: the place Airbnb actually searched (from its canonical URL).
canonical_urlstring
pagesintegerStatus rows: search pages read.
listingsintegerStatus rows: listings whose calendar was read.
monthsintegerStatus rows: occupancy rows delivered (and billed).
filteredintegerStatus rows: months dropped by minOccupancyPct — never billed.
ratesintegerStatus rows: months whose rate sample was priced.
duplicatesintegerStatus rows: listings already handled by another job in this run.
calendar_errorsinteger
rate_errorsinteger
detail_errorsinteger
errorstringStatus rows: the failure message, null when the job succeeded.
fetched_atstringUTC 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 adults guests. A month with weekend premiums or a stay priced mid-month can differ; run adults and months to 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; months is 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 status rows 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

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