Airbnb has no public API. Its API is for property-management and channel-management software that hosts connect their listings to, and Airbnb isn't taking new partners. To read listings and calendars you don't manage, you read Airbnb's public pages. Our API does that, and the script below turns a city into a CSV of how booked its listings look.
What Airbnb's API is for
Airbnb's partner API lets hospitality software push rates, availability and listing content for the hosts it manages, and receive their bookings and messages. Its FAQ says Airbnb is "not accepting new access requests". Nothing in it searches listings, so a search or a calendar for a stay you don't manage has to come from Airbnb's public pages.
Airbnb listings with ToolzerHub
We make ToolzerHub. Our Airbnb API has three routes, each a GET with the x-api-key header at 1 credit a call:
/v1/airbnb/stays/search?destination=...returns stays in a place, with optionalcheckin,checkoutand guest counts./v1/airbnb/stay/detail?stay_id=...returns a listing's property type, capacity, amenities, host stats and rating per category./v1/airbnb/stay/availability?stay_id=...returns each day for up to 12 months.
export TOOLZERHUB_API_KEY=your_key
curl -G -H "x-api-key: $TOOLZERHUB_API_KEY" \
--data-urlencode "destination=Kyoto, Japan" -d count=10 \
"https://api.toolzerhub.com/v1/airbnb/stays/search"
The first stay in data.stays, without its photo list:
{
"id": "1744088620623845596",
"url": "https://www.airbnb.com/rooms/1744088620623845596",
"name": "LuckyaKotako|Shijo&Nishiki|Shigaraki Open-Air Bath",
"title": "Townhouse in Kyoto",
"subtitle": "112 m² machiya with a private Japanese garden and a semi-open-air Shigaraki bath",
"coordinate": { "latitude": 35.0061, "longitude": 135.7554 },
"price": { "label": "$1,477 for 5 nights", "amount": "$1,477", "original_amount": null, "qualifier": "for 5 nights" },
"rating": { "score": 5, "reviews": 8, "label": "5.0 (8)" }
}
With no dates, every price is for a 5-night stay; send checkin and checkout for your own. A day in the same listing's calendar:
{ "date": "2026-10-24", "available": true, "bookable": true, "available_for_checkin": true, "available_for_checkout": false, "min_nights": 1, "max_nights": 1125 }
The calendar starts on the 1st of the current month and days before today come back as available: false, so count from today. It has no prices, and a closed day can be a booking or a day the host blocked, so "closed" is an upper bound on bookings.
Check how booked a set of listings is
The script takes a place, or a file of stay ids, and for each stay pulls 4 months of calendar plus its property type. It appends one row per stay: closed nights in the next 30 and 90 days, today's minimum stay, rating, price and link. The detail call doubles the cost but is the reliable way to spot hotels. Save it as airbnb_calendar.py and install requests (pip install requests).
import csv
import os
import sys
import time
from datetime import date, timedelta
import requests
BASE = "https://api.toolzerhub.com/v1/airbnb"
HEADERS = {"x-api-key": os.environ.get("TOOLZERHUB_API_KEY", "")}
FIELDS = ["checked_on", "stay_id", "type", "title", "name", "rating", "reviews", "price",
"closed_next_30", "closed_next_90", "min_nights_today", "url"]
def get(path, params, tries=3):
# 429, 5xx and dropped connections are retried; failed calls cost no credits.
for attempt in range(tries):
last = attempt == tries - 1
try:
r = requests.get(f"{BASE}/{path}", headers=HEADERS, params=params, timeout=120)
except requests.RequestException:
if last:
raise
time.sleep(10 * (attempt + 1))
continue
if r.status_code in (429, 500, 502, 503, 504) and not last:
time.sleep(10 * (attempt + 1))
continue
r.raise_for_status()
return r.json()["data"]
def stays_for(place, count):
if os.path.isfile(place): # a file of stay ids, one per line: the same stays every run
ids = [line.strip() for line in open(place) if line.strip()]
return [{"id": i, "url": f"https://www.airbnb.com/rooms/{i}"} for i in ids]
found = get("stays/search", {"destination": place, "count": int(count)})
if not found or not found["stays"]:
sys.exit(f"Airbnb found no stays for {place!r}")
if not found["location"]["resolved"]: # unrecognised places can return unrelated stays
sys.exit(f"Airbnb did not recognise {place!r}; try 'City, Country'")
print(f"{len(found['stays'])} stays in {found['location']['canonical_location']}")
return found["stays"]
def closed(days, start, n):
# Unavailable nights in the n days from start: booked or blocked by the host.
window = {(start + timedelta(i)).isoformat() for i in range(n)}
seen = [d for d in days if d["date"] in window]
return sum(not d["available"] for d in seen), len(seen)
def main(place, count="20", out="airbnb_calendar.csv"):
stays = stays_for(place, count)
today, written = date.today(), 0
new_file = not os.path.exists(out)
with open(out, "a", newline="", encoding="utf-8-sig" if new_file else "utf-8") as f:
writer = csv.DictWriter(f, fieldnames=FIELDS)
if new_file:
writer.writeheader()
for s in stays:
try: # 4 months from the 1st of this month covers the next 90 days
cal = get("stay/availability", {"stay_id": s["id"], "months": 4})
except requests.RequestException as e:
print(f"{s['id']}: calendar failed ({e})")
continue
if not cal:
print(f"{s['id']}: no calendar")
continue
try: # search mixes in hotel rooms; property_type says which (HOTEL)
d = get("stay/detail", {"stay_id": s["id"]}) or {}
except requests.RequestException:
d = {}
c30, n30 = closed(cal["days"], today, 30)
c90, n90 = closed(cal["days"], today, 90)
first = next((x for x in cal["days"] if x["date"] == today.isoformat()), {})
rating = s.get("rating") or d.get("rating") or {}
writer.writerow({
"checked_on": today.isoformat(), "stay_id": s["id"],
"type": d.get("property_type"), "title": s.get("title"),
"name": s.get("name") or d.get("name"), "rating": rating.get("score"),
"reviews": rating.get("reviews"), "price": (s.get("price") or {}).get("label"),
"closed_next_30": f"{c30}/{n30}", "closed_next_90": f"{c90}/{n90}",
"min_nights_today": first.get("min_nights"), "url": s["url"],
})
written += 1
print(f"{written} of {len(stays)} stays appended to {out}")
if __name__ == "__main__":
if len(sys.argv) < 2:
sys.exit('usage: python airbnb_calendar.py "Lisbon, Portugal" [count] [file.csv]\n'
' or: python airbnb_calendar.py stay_ids.txt 0 [file.csv]')
if not HEADERS["x-api-key"]:
sys.exit("set TOOLZERHUB_API_KEY first")
main(*sys.argv[1:])
python airbnb_calendar.py "Lisbon, Portugal" 20 lisbon.csv makes 41 calls, one search plus two per stay, and takes 44 to 54 seconds. We ran it on Lisbon and Austin three times each.
What the calendars showed
The share of the next 30 nights that was closed, per search:
| Search | Hotels in top 20 | Closed, other stays | Closed, hotels |
|---|---|---|---|
| Lisbon, run 1 | 4 | 62.5% | 25.8% |
| Lisbon, run 2 | 4 | 64.0% | 25.8% |
| Lisbon, run 3 | 12 | 74.2% | 16.1% |
| Austin, run 1 | 11 | 50.7% | 27.0% |
| Austin, run 2 | 2 | 55.7% | 21.7% |
| Austin, run 3 | 11 | 42.2% | 27.3% |
"Other stays" are everything the detail call didn't mark HOTEL: apartments, houses and the odd hostel. In every run the hotels' calendars were far emptier, 16% to 27% closed against 42% to 74%. Over 90 days the gap held: 50.3% to 57.2% closed for Lisbon's other stays and 6.8% to 13.3% for its hotels.
These are the top 20 results of a search, not a sample of a market, and the top 20 moves: two searches of the same city a few minutes apart shared 7 of 20 stays, in both cities. One search's average says little about a city.
Keep hotels and reshuffles out of the numbers
Drop rows where type is HOTEL before you average. The search subtitle ("Hotel in …") catches most hotels, but one Austin hotel titled "Room in Austin" only showed in the detail call's property_type.
Then save the ids of the stays you track, one per line, and run python airbnb_calendar.py stay_ids.txt 0 tracked.csv each week. A rerun of 20 saved Austin ids returned all 20, and the checked_on column shows how the same calendars fill up.
What it costs
A city search of 20 stays is 41 credits and a weekly run of 20 fixed stays is 40, about 175 credits a month. The 100 free credits a month cover the first two weeks; $19 buys 10,000 credits that never expire, and plans for higher volume cover more cities. Set your key and run the script on your city; the first CSV is ready in under a minute.
For prices from other public pages, see our Skyscanner API post and Zillow listings to CSV.
We are not affiliated with Airbnb.