Airbnb listings scraper and price tracker: scrape Airbnb search results for any location and dates — nightly and total price with taxes, rating, number of reviews, property type, coordinates and photos. Track price changes between runs. One flat row per listing, no personal data.
History of airbnb-listings-scraper (Apify id FEyfCaGZt8QhTDD76, folder actors/airbnb-listings). Versions are the Apify build numbers; entries before this file existed were reconstructed from the project journal on 2026-10-19.
Unreleased (all-in price: taxesIncluded and priceTotalAllIn)
Brief docs/cloud-tasks/2026-10-23_travel-prices-all-in.md (2026-10-23): rows are travel-listing/1.5, two columns after taxesAndFees, nothing else changes (priceTotal keeps its meaning: the total Airbnb displays, after discounts).
taxesIncluded from Airbnb's own price style (parsePrice › taxesIncludedOf): true for REGULATED_TOTAL (European sites, "au total": in the committed airbnb.fr captures the "Total" line is the sum of the lines above it, the "Taxes" line included, tested card by card), false for TOTAL_ONLY (airbnb.com, "for 3 nights": every card whose total Airbnb speaks says "total before taxes"), null for any other style, for a breakdown that contradicts the style (a taxes line under TOTAL_ONLY, "before taxes" under REGULATED_TOTAL), or without a price.
priceTotalAllIn = priceTotal when taxesIncluded is true, null otherwise (allInPriceTotal of @scraper-studio/core/schema): Airbnb's taxes and service-fee lines are parts of the total, never an amount left out, so nothing is added and the taxes of an airbnb.com total are never estimated. With the default domain (www.airbnb.com), priceTotalAllIn is therefore null; a European domain fills it.
The three __traces__/*.json re-recorded: only the version tag and the two new columns of the rows differ (checked by a diff of the files).
Dataset schema: both columns documented; Overview shows priceTotalAllIn next to priceTotal, Prices shows both after taxesAndFees. README: Which price to compare paragraph (with the domain advice), sample output with taxesIncluded (instead of roomType), the price notes, the strengths worth knowing when switching (hard maxResults cap never charged beyond, onlyChanges), More scrapers pointing at priceTotalAllIn.
Unreleased (list-first input for AI agents)
Brief docs/cloud-tasks/2026-10-23_travel-hotels-locations-first.md (2026-10-23), after Apify's post on Actors dropped by agents for a string primary input ("Make your primary input an array"): locations is the first field of the form, titled Locations, prefilled with ["Lisbon, Portugal"] (default still []), its description starting with "One or more …".
location kept (example tasks, daily checks and API callers use it), titled Single location, moved below startUrls, without prefill; description "Single destination (kept for compatibility; prefer locations)…". No field is both required and with a default; every description stays under 500 characters.
inputs/daily-check.json = the new prefilled form: locations instead of location, as runInputQualityChecks requires (the daily check runs what Apify's daily test runs). Same search, same rows and charges; the daily-check task is updated at the next release.
Input form texts: the form description starts with "Enter one or more locations"; maxResults refers to the new title Locations.
README: the Input section (introduction, example, table) shows locations first; nothing else changes.
Tests: the prefilled form is locations alone, searched once, non-empty within maxResults on the fixtures and with the rows of the former prefilled form (location alone). No code change: location, locations or both are accepted as before.
Unreleased (quality score: input documented for AI agents)
Brief docs/cloud-tasks/2026-10-20_studio-quality-score-insights.md (2026-10-21): the input form passes the shared checks of runInputQualityChecks and the Store texts those of runSearchTextChecks.
Descriptions rewritten (what the field does, an example of the format, the default and its reason): locations (shortened under 500 characters), startUrls (example URL; which URL filters are kept and which fields replace them, as search.ts does), checkIn, checkOut, adults, children, infants, pets, maxResults, priceMin, priceMax, roomTypes, currency, locale, domain, monitorPrices, monitorStoreName, onlyChanges, proxyConfiguration, splitSearchArea, includeMapPins, maxRequests, delayMs, debugStoreHtml.
Bounds: maximum 1,000,000,000 on priceMin and priceMax (the code accepts any non-negative whole number; no realistic nightly price reaches the bound, even in VND or IDR).
maxResults prefilled with 20 (default unchanged, 100): the prefilled input that Apify's daily test runs is now exactly the input of our daily check.
inputs/daily-check.json reduced to {"location": "Lisbon, Portugal", "maxResults": 20}: the keys equal to the defaults are dropped (adults, locale, domain, delayMs, proxyConfiguration), currency goes from EUR to the default USD and splitSearchArea from false to the default true (20 listings never saturate the 15 pages that trigger a split). The live daily-check task follows when the pilot re-sends it.
Store texts: store.json › searchSynonyms lists "Airbnb scraper", "Airbnb price tracker", "vacation rental" and "short-term rental"; the opening paragraph of the README says "vacation rental and short-term rental data" (was hyphenated) so that both phrases are in the head the Store search indexes. README input table: startUrls, maxResults (prefill), priceMin, priceMax.
No code change: rows, logs and charges are identical.
Unreleased (stay dates and comparable prices)
Relative stay (brief docs/cloud-tasks/2026-10-20_airbnb-listings-extend-stay-dates.md, 2026-10-20, findings of the agent panel of 2026-10-20, model: the Booking Actor): new fields checkInDaysFromNow (0–365) and nights (1–90), both empty by default, used when checkIn / checkOut are empty: check-in that many days after the UTC day of the run, for that many nights (one of the two alone: check-in in 30 days, or 3 nights). Resolved once when the input is read; every destination and start URL of the run searches that stay (start-URL dates are replaced, as with fixed dates). Fixed dates win over the relative fields (the start log says they were ignored).
Without any date the search is unchanged (flexible one-week stays, same tasks, URLs and rows apart from the price fix below), and the start log now warns that each listing is priced for its own proposed stay, so the prices are not comparable; start URLs that carry their own dates are not concerned. The start log shows the stay: fixed, relative (with its definition) or flexible.
checkIn / checkOut / nights of every row are the stay really priced: the listing's own dates (flexible search), else the dates of the request Airbnb answered (rawParams of the embedded StaysSearch request, new parse.ts › pageStayOf), else the run's. Rows of a start URL with its own dates and no input dates now carry those dates (they were empty).
Price monitoring with a relative stay: the record identity holds stay: "relative:<days>:<nights>" instead of the dates of the day, so a daily schedule keeps one record and compares the total of the stay at a constant lead time (same number of nights, the dates move). The identity, the record key and the comparison of fixed and flexible searches are those of version 0.3 (keys checked against the previous code).
pricePerNight is now the nightly price after discounts, before taxes and itemised fees, on every card shape: when Airbnb shows the base nightly rate followed by discount lines (special offer, early-bird, long stay), the discount lines are spread over the nights (e.g. 84.00 € → 71.20 €); when Airbnb already shows the discounted nightly rate, it is unchanged. So priceTotal = pricePerNight × nights + taxesAndFees + cleaningFee to display rounding (checked on every committed fixture: gap < 1). It previously mixed the two meanings (13 of 118 fixture rows change; priceTotal, the price compared by monitoring, is unchanged).
priceBeforeDiscount is also filled when Airbnb shows the discount lines without a strike-through price: priceTotal + the discount lines (1 fixture row: 704 €).
bedrooms: a card that lists beds without a bedroom line now gives 1 for a private room and 0 for an entire studio / loft (6 fixture rows); shared and hotel rooms, and every card of a flexible search (Airbnb shows no bed line there), stay null.
Replayed traces re-recorded: only pricePerNight of the 3 listings with a discount line and one priceBeforeDiscount changed; URLs, statistics and log lines are identical. Dataset field descriptions, input schema, README (input table, What the prices mean, flexible dates FAQ, price monitoring) updated; new tests src/stay.test.ts.
Unreleased (README visibility)
README first screen and links (brief docs/cloud-tasks/2026-10-20_studio-readme-visibility.md, 2026-10-20). The Store search indexes only the first ~3,000 README characters, so they now hold: an opening sentence with the phrase agents search and its synonyms, a "No login, no cookies, no personal data" line, the Free-plan price in figures in How much ($3.00 per 1,000 listings; Apify's $5 monthly credit covers about 1,666), computed from pricing/2026-10-19_pay-per-event.json (Free tier of the primary event, one apify-actor-start event deducted), and a real output snippet (12 key columns of the README example row). Further down: Use it from your AI assistant (MCP URL https://mcp.apify.com?tools=myth84/<name> and a ready-to-copy prompt with a minimal input), Alternatives and switching (official API, home-made scraper, input-name mapping table, no competitor named), More scrapers by Flatrow Data Studio (Store links to the other Actors of the studio) with an invitation to rate the Actor (no counterpart), and the non-affiliation line at the very bottom. The cost section and the MCP cost bullet quote the same figures; the line 1 heading is unchanged. store.json now names its studio (travel), read by the link check. Checked by runStoreChecks (readmeVisibility): keyword in the opening prose, snippet equal to the example row and within 3,000 characters, figures equal to the pricing file in force (a new grid fails the test until the README follows), MCP block with a valid input, links to existing Actors of the same studio only, no fpr=. No code change: rows, logs and charges are identical.
Unreleased (several destinations, deployed first as a beta build)
Several destinations per run (brief docs/cloud-tasks/2026-10-20_airbnb-listings-extend-locations.md, 2026-10-20): new field locations (list, empty by default) next to location; the two are merged without duplicates (same text up to case and spacing), empty lines are ignored, at most 50 destinations per run (clear error beyond). Each destination is a separate search with the same dates, guests, filters and currency; its rows carry it in searchLocation.
With locations, maxResults applies to each destination (and to each start URL), like maxResults per search of the AliExpress Actor: a full destination stops without stopping the run, the charger allows maxResults × searches. Without locations, maxResults keeps capping the whole run (location and start URLs together) and the output, the charges and the log lines are unchanged (replayed traces, explicit tests).
A listing found by two destinations is written and charged once, under the first destination of the list; the listings a full destination saw but did not take stay available to the next destinations. With price monitoring, each destination keeps its own record (distinct key) with the listings it compared, and maxResults is the number of listings compared per destination.
Log: one line per destination at the end (listings, duplicates skipped, not searched when the run stopped before it, no listing found), a warning at start when maxRequests looks too low for the number of searches; RUN_STATS.searches (per destination) when locations is used.
README: input table, three-city input example, FAQ entry Can I search several cities in one run? (the export question, already covered by Integrations and API, was removed to keep 10 questions); searchLocation description updated in the dataset schema; one more example task (airbnb-paris-rome-barcelona-prices), 10 in total (the platform maximum).
Unreleased (version 0.3, deployed first as a beta build)
Price monitoring (brief docs/cloud-tasks/2026-10-20_travel-extend-monitor.md, 2026-10-20, model: the AliExpress Actor): monitorPrices (off by default), monitorStoreName (default airbnb-price-monitor) and onlyChanges; previous total prices kept in the customer's named key-value store, one record per search (location or URL, site, currency, language, dates, guests, filters; the quadrants of a split area share the record of their search); changeTypenew / price_changed / unchanged with previousPrice, priceDelta, priceDeltaPercent on every row; onlyChanges writes and charges only the new listings and the price changes; first run = reference (RUN_STATS.monitorBaseline), RUN_STATS.monitor summary and extra.monitor* counters; records written only after a run whose searches ended cleanly (fail-closed); a monitoring run without any change succeeds with an empty dataset; in monitoring mode maxResults is the number of listings compared. A price is compared only for the same currency and the same stay dates (flexible searches: listings whose proposed week moved are unchanged).
Output schema travel-listing/1.2 (additive): changeType, previousPrice, priceDelta, priceDeltaPercent appended to every row (null outside monitoring runs); new dataset view Changes. Without the new options the rows are identical to 0.2 apart from the new columns (replayed traces).
README: section Price monitoring, input table, FAQ entry; one more example task (airbnb-lisbon-price-monitor), 9 in total.
Unreleased (version 0.2, deployed first as a beta build)
Store texts (brief docs/cloud-tasks/2026-10-20_studio-promote-store-texts.md, 2026-10-20): README opens with what you get / for whom / how much, shows the real input of the validation run, a pricing table that points to the Pricing tab (no figure in the text), a For AI agents (MCP) section with a minimal input, the key output fields, the cost per 1,000 results and the typical time; FAQ kept to 8–10 questions; no proxy or anti-bot detail left in the texts visible on the Store. Tests on the Store lengths (seoTitle ≤ 60, seoDescription ≤ 155, description ≤ 300), the README structure and the forbidden mentions. No code change.
Internal: the crawl runs on the studio's generic paged-search crawler and Actor runner (@scraper-studio/core/crawl, /actor-runner); platform code is reduced to Airbnb hooks (search URL, parsing, cursors, quadrant splitting). Requests, rows and log lines are unchanged (replayed traces).
RUN_STATS: listingsSeen / listingsPushed / splits become itemsSeen / itemsPushed / extra.splits; new bytesTransferred, bytesTransferredEstimates; bytesSource can be gzip-estimate (wire bytes estimated by gzip when HTTP/2 gives no Content-Length), so the estimated cost per 1,000 is now close to the real transfer instead of the decompressed size.
No pause after the last page of a search (saves delayMs per search).
Tests: crawler covered end to end on committed fixtures (pagination, caps, deduplication, splitting, rotation).
Dataset schema now generated from the unified travel-listing schema and .actor/dataset-fields.json / .actor/dataset-views.json (scripts/gen-dataset-schema.mjs); generated JSON unchanged.
Store readiness tests moved to the shared @scraper-studio/core/testing checks (same coverage); this changelog referenced from actor.json.
defaultMemoryMbytes: 256 in actor.json (the Actor card already runs at 256 MB since 2026-10-19): applied at the next apify push.
0.1.6 — 2026-10-19
Actor output schema added (.actor/output_schema.json: listings, runStats), required by the Store to publish.
Published on the Apify Store (https://apify.com/myth84/airbnb-listings-scraper), category Travel, pay-per-event pricing; KYC validated the same day, listed in the default Store search.
Daily control run in place: task airbnb-listings-daily-check (Lisbon, flexible dates, 20 listings, datacenter proxies, 256 MB, 300 s), schedule every day at 06:15 Europe/Paris, e-mail alert through apify/send-mail on failed, timed-out or aborted runs.
0.1.5 — 2026-10-19
Pay-per-event counter fixed: ResultCharger read the SDK's total event count (which includes the free dataset event); it now reads the count of its own result event. Pricing guard assertPricingConsistent('result') at start-up.
Pay-per-event pricing set through the API: result 0.003 / 0.0015 / 0.0012 / 0.001 $ (FREE / BRONZE / SILVER / GOLD; PLATINUM and DIAMOND aligned on GOLD by Apify), apify-actor-start 0.00005 $, minimum budget 0.05 $ per run. Billing trial: 20 listings, 20 result events charged.
Store metadata set through the API: category TRAVEL, SEO title and description.
0.1.3 — 2026-10-19
Scale control run: 400 unique listings in 25 requests and 79 s, area splitting triggered after saturation (15 pages), 0 blocked, 0.0021 $ (≈ 0.005 $ per 1,000 listings).
0.1.1 – 0.1.2 — 2026-10-19
First deployment as airbnb-listings-scraper; validation run: 60 listings in 4 datacenter requests, 0.0008 $, no row rejected by the dataset schema. A first run had failed on forbidden characters in key-value store keys (debug mode): fixed.
Hotel cards (property and room type) handled; meaning of pricePerNight documented (base nightly rate before discounts, fees and taxes).
0.1 — 2026-10-19
First version, written offline on committed fixtures: embedded StaysSearch state parsing, normalisation to travel-listing/1.1 (listedBy added: EU trader status, never a person), one sticky session per search, cursor pagination, global deduplication, map-area splitting, map pins, pay-per-event result charging, RUN_STATS, explicit failure on empty output.
Store readiness delivered by cloud task n°1 (PR #1): README, input schema, dataset schema with Overview / Prices / Location views, consistency tests (README ⇔ schemas ⇔ code, Ajv replay of fixture rows).