If you rent out an apartment, the useful question is rarely "what is the average price in my city". It is "what are the listings a guest would compare with mine asking for the dates I still have open, and did they move overnight". An Airbnb price tracker answers that by repeating the same search every day and keeping only what changed. This tutorial builds one in Python with the Airbnb price tracker mode of the Airbnb Listings Scraper, an Apify Actor called through the official Python client: a daily search for one stay that flags every listing as new, changed or unchanged since the previous run, with the previous total and the difference.
What you need
- An Apify account (the free plan is enough to try this).
- Your API token, from Apify Console → Settings → API & Integrations, exported as an environment variable:
export APIFY_TOKEN=... - Python 3 and two packages:
pip install "apify-client>=3,<4" pandas
Step 1: pick the stay you care about
Airbnb prices are prices of a stay, not of a listing: the same apartment costs one thing for a Tuesday in February and another for New Year's Eve. The Actor offers two ways to pin the stay.
-
Fixed dates (
checkIn,checkOut): the tracker follows the total price of that exact stay until it happens. This is the right choice for peak dates you are pricing now. -
A relative stay (
checkInDaysFromNow,nights): each run searches the stay that starts N days later. The tracker follows the price at a constant lead time ("a 3-night stay starting in 30 days"), and the schedule never expires.
Without any date, Airbnb proposes its own stay for each listing and the prices are not comparable. For tracking, always give dates.
Step 2: the input
The public task "Monitor Airbnb prices in Lisbon daily" watches the 60 first Lisbon listings for two adults in euros, and its description says to add your own dates. Here they are, for three nights over New Year's Eve, with onlyChanges so that later runs return only what moved:
run_input = {
"location": "Lisbon, Portugal",
"checkIn": "2026-12-30",
"checkOut": "2027-01-02",
"adults": 2,
"currency": "EUR",
"maxResults": 60,
"monitorPrices": True,
"onlyChanges": True,
# Closer to your competitors: "roomTypes": ["entire_home"], "minBedrooms": 2,
}
The fields not shown keep the Actor's defaults (airbnb.com, English). With monitorPrices on, maxResults is the number of listings compared by each run. "Lisbon" is a whole city; your competitors are a neighbourhood and a size. Add the Actor's own filters (roomTypes, minBedrooms, priceMin / priceMax per night), or set the map area on Airbnb in your browser and pass the search URL in startUrls: filters that have no field in the input are kept, and the dates, guests and currency of the input are applied on top.
Step 3: the tracker script
The Actor stores the total price of every compared listing in a key-value store of your Apify account (airbnb-price-monitor by default, set by monitorStoreName), one record per search. Because the reference lives in your account, the script below can run from cron on any machine. Save it as track_airbnb.py:
import os
import pandas as pd
from apify_client import ApifyClient
HISTORY = "airbnb-lisbon-nye.csv"
run_input = {
"location": "Lisbon, Portugal",
"checkIn": "2026-12-30",
"checkOut": "2027-01-02",
"adults": 2,
"currency": "EUR",
"maxResults": 60,
"monitorPrices": True,
"onlyChanges": True,
}
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("myth84/airbnb-listings-scraper").call(run_input=run_input)
status = getattr(run.status, "value", run.status) # SUCCEEDED, FAILED, TIMED-OUT...
if status != "SUCCEEDED":
raise SystemExit(f"The run ended as {status}: open its log in Apify Console.")
rows = list(client.dataset(run.default_dataset_id).iterate_items())
if not rows:
print("Nothing moved since the previous run.")
raise SystemExit(0)
for item in rows:
if item["changeType"] == "price_changed":
print(f'{item["title"]}: {item.get("previousPrice")} -> {item["priceTotal"]} '
f'{item["currency"]} ({item.get("priceDeltaPercent")} %) {item["url"]}')
else:
print(f'new: {item["title"]}, {item.get("priceTotal")} {item["currency"]}')
df = pd.DataFrame(rows)
df.to_csv(HISTORY, mode="a", header=not os.path.exists(HISTORY), index=False)
The first run of a search is the reference: every listing comes back with changeType set to "new". From the second run on, with onlyChanges, the dataset holds only the listings that were not in the record and the listings whose total changed, with previousPrice, priceDelta and priceDeltaPercent. Only those rows are saved and charged. A morning where nothing moved gives a successful run with an empty dataset; every other empty run fails with an explicit message in its log, so a misspelt location never passes for a quiet market.
The compared price is priceTotal, the total of the stay shown on the card after Airbnb's discounts. On airbnb.com that total may exclude taxes; on European Airbnb sites (set domain to www.airbnb.fr, www.airbnb.de and so on) it is the regulated total including taxes and fees, itemised in taxesAndFees. Pick one site and keep it.
Step 4: what a host reads in the history
After a couple of weeks, the CSV tells a story. Count the moves per morning and put your own total next to the market:
import pandas as pd
MY_TOTAL = 620 # your own total for the same stay, in EUR
history = pd.read_csv("airbnb-lisbon-nye.csv")
history["day"] = history["scrapedAt"].str[:10]
history["move"] = "new"
history.loc[history["priceDelta"] < 0, "move"] = "drop"
history.loc[history["priceDelta"] > 0, "move"] = "rise"
print(pd.crosstab(history["day"], history["move"]))
first_day = history[history["day"] == history["day"].min()]
print("Market median on day one:", first_day["priceTotal"].median(), "| mine:", MY_TOTAL)
print(history[history["move"] == "drop"].nsmallest(5, "priceDeltaPercent")[["day", "title", "priceDeltaPercent"]])
The first day is the full reference set, so its median is the market level when you started. After that:
| What you see | What it usually means |
|---|---|
| Several drops on the same morning, weeks before the stay | competitors cutting prices for dates that are not filling |
| Rises concentrated in the last days | the remaining supply is getting scarce |
A steady trickle of new rows |
listings appearing for those dates: new supply, or hosts opening those dates |
priceBeforeDiscount on a changed row |
the drop is a discount with a strike-through price, not a new base price |
Step 5: schedule it
With cron, one line runs the script at 7:00 every day (make sure APIFY_TOKEN is set in the cron environment):
0 7 * * * cd /path/to/tracker && python3 track_airbnb.py >> tracker.log 2>&1
Without code: open the public task in Apify Console, add your dates, tick onlyChanges, save it as your own task and give it a daily schedule; the Changes view of the Output tab shows the change type, the totals and the difference. To watch several peaks, make one task or script per stay; they can share the same monitorStoreName, since each search has its own record.
Honest limits
-
The tracker watches a search, not a list of listings. The compared set is the first
maxResultslistings, and Airbnb reshuffles its ranking between sessions. A listing that drops out and comes back is not reported as new: its last price is kept for 90 days. - No "booked" signal. A listing that disappears produces no row; it may be booked or just ranked lower that morning.
- Same stay and currency only. A total is compared with a previous total for the same dates and currency.
- A bad run never erases your reference. A failed or interrupted run does not overwrite the record.
- No personal data. Rows hold listing facts, the average rating and the review count; nothing about hosts or guests.
Pricing is pay-per-event: a fixed price per listing saved (see the Pricing tab of the Actor), and with onlyChanges only the new and changed listings count. Use the data for analysis, respect the platform's terms and local law, and do not republish listings.
Disclosure: I build these Actors and may earn a commission if you sign up to Apify through these links.
Top comments (0)