A room can disappear from a short Airbnb search export even when nothing about its availability has changed. Search order changes, and a three-result limit only captures three observations. Treating every missing room as booked would turn that sampling limit into a false alert.
This Python and SQLite workflow keeps price observations for one fixed stay. It compares a room only with its earlier observation for the same destination, dates, guest counts and currency. Missing rooms keep their history. A missing numeric price stays null.
I maintain the Airbnb Search Scraper used here. The code, offline check, input and dated sample are available together. The script needs Python 3.11 or newer and uses the standard library.
What the two real captures showed
On September 10, 2026, I captured three public search results for Berlin, September 18–20, two adults, EUR, then repeated that exact search about five minutes later. Both runs used the existing Actor build 1.0.6. Each returned three records and stopped at the result limit.
| Room ID | First observed total | Second observed total | Interpretation |
|---|---|---|---|
721527807549293451 |
€239 | €239 | Same observed price |
19976281 |
€257 | €257 | Same observed price |
26180749 |
€240 | Not in this export | Keep the earlier observation |
23848408 |
Not in the first export | €286 | First observation |
The database contains six observations across four room IDs. No price change was observed in this small live example. The offline check separately exercises a price decrease, missing prices, invalid stay data and failed imports; those checks are not additional market observations. The download airbnb-stay-sample.jsonl contains the six dated observations and their comparison fields.
The source labels these prices “for 2 nights.” They are observed search totals, not nightly prices or confirmed checkout quotes. Fees, availability and the final payable amount can change. This sample is dated evidence, not a current booking recommendation.
Keep the stay fixed
Download airbnb_stay_watch.py, test_airbnb_stay_watch.py and airbnb-stay-input.json from the Gist into the same directory. The input used above is:
{
"location": "Berlin, Germany",
"checkIn": "2026-09-18",
"checkOut": "2026-09-20",
"adults": 2,
"children": 0,
"infants": 0,
"pets": 0,
"currency": "EUR",
"maxResults": 3,
"maxPages": 1
}
Choose future dates before your first capture. Use a new output folder when changing any input, including the page or result limit. Rolling dates such as “seven days from now” make successive prices describe different stays, so this workflow requires explicit dates.
Run the check before using your token:
python test_airbnb_stay_watch.py
It runs offline and starts no Actor. Set APIFY_TOKEN through your environment or secret manager; keep it out of the input file and downloaded code. Then capture your chosen stay:
python airbnb_stay_watch.py capture berlin-stay --input airbnb-stay-input.json
Keep the input file outside berlin-stay. The folder contains stays.sqlite3, stay-history.jsonl and completed run receipts. Back it up if the history matters. Run commands sequentially against one folder.
Later, repeat the same capture command. That starts a new paid Actor run for the same search and adds observations for its returned rooms. The second real capture printed:
{
"new_observations": 3,
"comparisons": {"same_price": 2, "first_observation": 1},
"source_stop_reason": "result_limit",
"stored_observations": 6,
"stored_runs": 2
}
Read the history without inventing changes
Each JSONL line contains the source listing, run ID, comparison status, previous total and price delta. The statuses are:
| Status | Meaning |
|---|---|
first_observation |
This room has no previous observation in this folder |
same_price |
Both numeric totals are present and equal |
price_changed |
Both totals are present and differ; price_delta is current minus previous |
unpriced |
At least one of the two observations lacks a numeric total; no numeric delta is inferred |
The importer checks the room ID and URL, destination, dates, all guest counts, currency and source price labels before writing. It uses decimal arithmetic for price differences and rejects duplicate room IDs within one run. An invalid row rejects the entire import, preserving the earlier database history.
A missing room creates no replacement record. Its earlier price remains a historical observation, not a claim that it is still available. Even an unchanged numeric price says nothing about cancellation terms, minimum-stay rules or checkout availability.
Regenerate the JSONL file from SQLite without making an API request:
python airbnb_stay_watch.py export berlin-stay
Recover a completed run before starting another
In the first live capture, the finished run's dataset export was not immediately consistent with its summary. The importer rejected the incomplete read and kept the pending run receipt. Once the export was available, I resumed that same run, without starting another.
The script now retries dataset reads up to three times, two seconds apart. It never automatically repeats a start request. If the export still is not ready, use the run ID printed by the script:
python airbnb_stay_watch.py capture berlin-stay --input airbnb-stay-input.json --run-id YOUR_EXISTING_RUN_ID
This path only reads the existing run. It validates the saved input before importing anything. Reimporting an already imported run adds zero observations; the real second run was resumed this way and its JSONL export remained byte-identical.
If a start request times out before a run ID is received, pending.json prevents another automatic start. Inspect your Apify runs for that request's time and exact input, then resume the matching run ID. Do not delete the pending receipt and blindly repeat the start. Failed runs leave the history intact and need inspection before a new capture.
Cost and scope
At the September 10 Free-tier price, three exported records cost $0.015, plus $0.00005 for a 512 MB start: $0.01505 per capture. The two demonstrated captures have a calculated Free-tier Actor-price equivalent of $0.03010. Repeat observations are still billable results; SQLite prevents duplicate imports, not charges for fresh source runs. Check the Actor's Pricing tab for your current tier.
Every start made by this example sets a $0.06 maximum Actor charge, 512 MB and a 300-second timeout. The wrapper accepts at most ten results and two source pages. A spending or result limit may leave the search incomplete. The cap applies to Actor charges; separate platform or local storage costs are outside it. These demonstrations were owner tests, not customer sales.
This workflow does not book rooms, send notifications or create a recurring schedule. For long-term rental change alerts, the Zumper workflow covers a different source and monitoring contract. Use the fixed-stay workflow here when dates and occupancy are part of the price you need to compare.
Top comments (0)