DEV Community

Ben
Ben

Posted on

Track Airbnb stay prices without mistaking missing results for bookings

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)