DEV Community

Matvey
Matvey

Posted on

Two Seminyak Villas Went Fully Booked on the Same Day. It Was Day 180

I run a visa agency in Bali, which means I spend a lot of time looking at how full the island is. One of the Apify Actors I maintain reads Airbnb's availability calendar — the grid you see when you open the date picker on a listing — and returns it as rows: free nights, taken nights, the host's minimum stay per date, occupancy per month, up to twelve months ahead.

Yesterday I pointed it at my own neighbourhood. Twenty listings, a full year, everything on:

{
  "locationQueries": ["Seminyak, Bali, Indonesia"],
  "maxListings": 20,
  "calendarMonths": 12,
  "includeDays": true
}
Enter fullscreen mode Exit fullscreen mode

22.9 seconds, 20 rows, $0.05 for the lot. The calendars run from 2026-10-30 to 2027-09-30 — 336 nights each, 6,720 nights in total. Mean occupancy across the twenty: 33.9%.

Then I sorted by occupancy and the top row stopped me.

CONTEMPORARY POOL HOUSE — Seminyak
occupancyPercent: 94.3
availableDays:    19
bookedDays:       317
Enter fullscreen mode Exit fullscreen mode

A Seminyak villa booked solid for a year, in a market where the other nineteen averaged 30%. Its monthly breakdown was worse:

2026-11   86.7%
2026-12   51.6%
2027-01  100.0%
2027-02  100.0%
2027-03  100.0%
2027-04  100.0%  ... and so on to 2027-09
Enter fullscreen mode Exit fullscreen mode

Nine consecutive months at exactly 100%. Nobody books nine months straight. The last night this listing had free was 2026-12-15, and every one of the 289 nights after it came back unavailable.

Two listings, the same wall, the same date

Once I knew what to look for, I measured the same thing on all twenty: find the last night that is free, and count how many unavailable nights sit between it and the end of the window.

listing                          occ 12m   last free night   tail nights
CONTEMPORARY POOL HOUSE            94.3     2026-12-15          289
Villa 1 Cable                      58.3     2027-04-28          155
Villa padi bali one bedroom        55.4     2027-04-28          155
Tropical Boutique 2 BDR Villa      46.7     2027-07-27           65
Stylish 2 bedroom poolvilla        42.9     2027-06-30           92
(the other 15)                      ...     2027-09-30            0
Enter fullscreen mode Exit fullscreen mode

Two different villas, different hosts, different prices, stop being bookable on exactly the same night: 29 April 2027. The last night either of them sells is 2027-04-28 — exactly 180 days after the first night in the window, to the day, on both.

Two unrelated properties are not booked solid from the same April evening through the following September. That date is a setting. Airbnb lets a host cap how far ahead guests may book — three months, six, nine, twelve, or all future dates — and both of these hosts picked six months. Every night past the cap is served to me as unavailable, identical to a night that someone actually paid for.

The other two walls fit the same menu: 2027-06-30 is eight months out and 2027-07-27 is about nine. The POOL HOUSE at 46 days is not a menu entry, so I cannot claim a setting there — it may be a host who blocks the calendar by hand, or one who really is sold out. Either way, nine straight months at 100% is not an occupancy figure anyone should put in a model.

Nothing in the row tells you which is which

My first instinct was that the boundary must be visible in some field. It is not. Here is the crossing on Villa padi bali, the night before the wall and the three after:

{"date":"2027-04-28","available":true, "bookable":true, "minNights":2,"maxNights":100,"checkOut":true}
{"date":"2027-04-29","available":false,"bookable":false,"minNights":2,"maxNights":100,"checkOut":true}
{"date":"2027-04-30","available":false,"bookable":false,"minNights":2,"maxNights":100,"checkOut":false}
{"date":"2027-05-01","available":false,"bookable":false,"minNights":2,"maxNights":100,"checkOut":false}
Enter fullscreen mode Exit fullscreen mode

I compared the full set of minNights / maxNights / checkOut combinations in the 155 tail nights against the same set in the genuinely booked nights earlier in the year. They are the same set. Same minimum stay, same maximum, same check-out flags. Airbnb is not withholding a field — the calendar endpoint simply has one boolean for "you cannot have this night", and it covers both "sold" and "never offered".

So the only signal is shape: an unbroken run of unavailable nights that reaches the end of the requested window. That is cheap to detect and you should detect it, because the alternative is quietly wrong numbers.

def last_open_night(calendar):
    free = [d["date"] for d in calendar if d["available"]]
    return free[-1] if free else None

# occupancy measured only up to the horizon the host actually opened
def honest_occupancy(calendar):
    free = [i for i, d in enumerate(calendar) if d["available"]]
    if not free:
        return None                      # listing sells nothing in this window
    window = calendar[: free[-1] + 1]
    booked = sum(1 for d in window if not d["available"])
    return round(booked / len(window) * 100, 1), len(calendar) - len(window)
Enter fullscreen mode Exit fullscreen mode

It is a few lines, and it is the difference between a demand curve and a fiction. Aggregating all twenty listings month by month, raw against trimmed:

          raw     trimmed   listings still open
2026-11   64.2%    64.2%        20
2026-12   40.8%    39.2%        20
2027-01   35.2%    31.7%        19
2027-02   31.6%    28.0%        19
2027-04   24.5%    20.0%        19
2027-06   26.3%    13.3%        17
2027-07   24.5%     4.9%        16
2027-08   31.5%     8.6%        15
2027-09   30.7%     7.6%        15
Enter fullscreen mode Exit fullscreen mode

The raw curve says Seminyak sits around 30% booked next August. The trimmed one says 8.6%, across the fifteen listings that are actually selling August at all. Five times off, and the error grows exactly where it is least visible — in the far months, where a revenue model most wants a signal and where a human would never check by hand.

The honest reading of the raw curve's flat tail is not "steady demand". It is "my sample is shrinking and I am counting closed shops as full ones".

The other field that is not what it says

While I was in there, a second one. Each row carries a listing-level minNights, which I compute as the smallest minimum stay found anywhere in the window. The POOL HOUSE reports:

{ "minNights": 1, "maxNights": 1125, "daysScanned": 336 }
Enter fullscreen mode Exit fullscreen mode

Across its 336 nights, 328 say the minimum stay is 3. Exactly two say 1 — 14 and 15 November. Those two nights are a hole:

{"date":"2026-11-13","available":false,"minNights":3,"maxNights":1125}
{"date":"2026-11-14","available":true, "minNights":1,"maxNights":10}
{"date":"2026-11-15","available":true, "minNights":1,"maxNights":10}
{"date":"2026-11-16","available":false,"minNights":3,"maxNights":1125}
Enter fullscreen mode Exit fullscreen mode

Airbnb rewrites the minimum downward to fit gaps between bookings, and the maximum with it — 1125 nights becomes 10. So the listing-level 1 is not the host's policy. It is the size of a two-night hole, and it is gone the moment someone books it.

Six of my twenty listings have a top-level minNights that applies to under 10% of their year. One sells a 1 that is true for 2 nights out of 336. Filtering the twenty by minNights <= 1 keeps 13 listings; only 7 of those really run on single nights, judged by the value that covers most of their calendar. Worth knowing before you build "one-night stays in Seminyak" on top of that field.

A smaller one in the same place: of the 196 nights whose minimum sits below their listing's usual value, 52 are on nights that are already booked. Minimum-stay rules on a sold night are noise — Airbnb keeps serving them, and they will skew any average you take over the raw array. Filter on available first.

Calling it

One request, no browser, no login — the Actor asks Airbnb the same question the listing page asks, which is why twelve months cost the same as one:

curl -X POST "https://api.apify.com/v2/acts/lergassy~airbnb-availability-calendar/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"startUrls":[{"url":"https://www.airbnb.com/rooms/52869271"}],"calendarMonths":12}'
Enter fullscreen mode Exit fullscreen mode

That is Airbnb Availability Calendar, $0.0025 per listing regardless of how many months you read. Set includeDays to false and you get one compact row with monthly occupancy and no day array, which is usually what a dashboard wants — but if you are doing the trimming above, keep the days, because the horizon is only visible in them.

When it is the wrong tool. It returns no prices. Airbnb stopped sending nightly rates in the calendar response, so no calendar reader can give you a price per date without inventing it; for the cost of a stay you have to price specific check-in and check-out dates, which is a different request. It also will not tell you whether a booked night was booked yesterday or last March — you get a snapshot, and velocity only exists if you store snapshots and diff them. And twelve months is a ceiling, not a promise: as measured above, a quarter of my sample stopped selling well before it.

The flag says available: false. It has always meant two things. Count them as one and your summer disappears.

Top comments (0)