# STR Market Sample: short-term rental market rollup (`s-r/str-market-sample`) Actor

One row per market with a short-term rental rollup: medians and means with their denominators for nightly rate, occupancy days and revenue, a room and bedroom mix, and a month-by-month occupancy curve. A US address adds the live estimate alongside. Two indexes, never averaged. Aggregates only.

- **URL**: https://apify.com/s-r/str-market-sample.md
- **Developed by:** [SR](https://apify.com/s-r) (community)
- **Categories:** Real estate, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$50.00 / 1,000 market rolled ups

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Airbnb Revenue Estimator Actor: short-term rental market rollup with seasonality

This Airbnb revenue estimator actor returns one row per market with a
short-term rental market rollup: nightly rate, occupancy days and revenue as
medians and means with their denominators named, a room and bedroom mix, a
neighbourhood mix, and a month-by-month booked-share curve. Give it a market
name for the published snapshot rollup, and add a US address for the live
estimate alongside it. The two indexes sit side by side with their own dates
and are never averaged. Aggregates, attribution and the snapshot date only.

### What you get

- One dataset row per market, so sizing forty cities is billable only for the
  markets you actually received.
- Nightly rate as a full distribution: count, median, mean, 25th and 75th
  percentile. Not one number pretending to be the market.
- Occupancy days over the last year and availability over the next, each with
  its own distribution.
- The revenue block, which is where most tools go wrong. Median as the
  headline, and all three ways of averaging the column reported beside the
  count each was taken over: the mean over rows carrying a revenue figure, the
  mean over rows with a revenue figure above zero, and the mean over every
  listing in the file with missing counted as zero. The filter is pinned in
  the code and stated on the row, so no figure can be quoted without its
  denominator.
- Room mix (entire home, private room, shared room, hotel room), bedroom mix
  (studio to 7 plus) and the ten largest neighbourhoods by listing count.
- A month-by-month curve: listed days and booked share per month, aggregated
  over every listing's calendar.
- The live estimate for a US address: average daily rate, occupancy
  percentage, monthly revenue, revpan, the 25th and 75th percentile bands for
  each, the seasonalised annual revenue with its bands, a twelve-month revenue
  curve, and the number of comparables behind it.
- `snapshot_as_of` on the row: the date the published snapshot was taken, not
  the date you ran the Actor. A six-week-old snapshot is a fact you should see.
- Required attribution on the row, as the data licence asks.
- A `data_coverage` count block saying which of the three blocks returned
  figures, and `indexes_are_never_averaged` set to true so no consumer
  silently blends a June snapshot with a live estimate.

### Why scrape short-term rental market data

An investor about to sign a lease on an arbitrage deal needs to know what the
market pays before the lease exists. A lender or an insurer pricing short-let
risk needs the same numbers, at market grain rather than listing grain. A
platform sizing a new city needs the distribution, not the average.

The problem with the tools that already answer this is the shape of the
answer. A free calculator returns one estimate and an email funnel. A
subscription product returns the same numbers behind a login and a plan. A
listing scraper returns ten thousand rows and no rollup, leaving the buyer to
work out the average themselves, which is where the trouble starts.

It starts because the revenue column has three honest averages. On a real
market snapshot of just over ten thousand listings, the mean over rows that
carry a revenue figure is 18,297. The mean over all listings with missing
counted as zero is 11,253. The mean over nonzero rows only is 23,350. Those
are three different questions. A tool that reports one of them without
naming its denominator hands a lender a number that will not survive
scrutiny, and an investor a number that will not survive a spreadsheet.

So this Actor reports the median as the headline and puts every mean beside
the count it was taken over. That single decision is the difference between a
figure you can put in a committee paper and a figure you cannot.

### Input

| Field | Required | What it does |
|---|---|---|
| `markets` | Yes | Markets to roll up. City name, or `city, country` to disambiguate. Up to 20 per run. |
| `address` | No | A US street address for the live tier. The live tier is asked once per run regardless of how many markets you list. |
| `include_snapshot` | No | Aggregate the published snapshot. On by default. |
| `include_seasonality` | No | Month-by-month booked share. On by default. |
| `include_live` | No | Live estimate for the address. On by default, skipped when no address is given. |
| `min_bedrooms` | No | Narrow the live tier's comparables by bedroom count. Default 1. |
| `max_bedrooms` | No | Upper bound on bedrooms for the live tier. Default 5. |
| `max_markets` | No | Upper bound on markets per run, 1 to 20. Default 5. |

### Output

```json
{
  "market": "Amsterdam",
  "as_of": "2026-09-25",
  "snapshot_as_of": "2026-06-15",
  "snapshot_index": {
    "grain": "market",
    "listing_count": 10369,
    "price": { "count": 6377, "median": 291.0, "mean": 346.62, "p25": 206.28, "p75": 411.0 },
    "occupancy_days_l365d": { "count": 10369, "median": 16.0, "mean": 48.31 },
    "revenue_l365d": {
      "count": 6377,
      "median": 9496.0,
      "mean_with_figure": 18296.92,
      "mean_with_figure_count": 6377,
      "mean_nonzero": 23349.9,
      "mean_nonzero_count": 4997,
      "mean_all_listings_missing_as_zero": 11252.72,
      "mean_all_listings_count": 10369,
      "filter": "median over listings carrying a revenue figure; every mean named with its denominator"
    },
    "room_mix": { "Entire home/apt": 8489, "Private room": 1833, "Hotel room": 26, "Shared room": 21 },
    "bedroom_mix": { "1": 5250, "2": 2540, "studio_or_0": 1154, "3": 1057 },
    "neighbourhood_mix": { "De Baarsjes - Oud-West": 1818, "De Pijp - Rivierenbuurt": 1195 }
  },
  "seasonality": {
    "grain": "market",
    "day_count": 3819725,
    "months": {
      "2026-06": { "days_listed": 128962, "available_share": 0.2072, "booked_share": 0.7928 },
      "2026-07": { "days_listed": 324415, "available_share": 0.2508, "booked_share": 0.7492 }
    }
  },
  "live_index": {
    "grain": "market",
    "status": "ok",
    "address": "200 E 6th St, Austin, TX 78701",
    "comparable_count": 97,
    "adr_average": 410.39,
    "adr_p25": 257.0,
    "adr_p75": 531.0,
    "occupancy_pct_average": 49,
    "monthly_revenue_average": 5749.98,
    "seasonalized_annual_revenue_average": 66262.1,
    "seasonalized_revenue_by_month": []
  },
  "indexes_are_never_averaged": true,
  "field_grain": {
    "snapshot_index": "market",
    "seasonality": "market",
    "live_index": "market"
  },
  "data_coverage": { "snapshot": 1, "seasonality": 1, "live": 1 },
  "missing_blocks": [],
  "attribution": {
    "snapshot": "Market snapshot data by Inside Airbnb, licensed CC BY 4.0",
    "live": "Live estimate figures as published by the calculator at rabbu.com"
  }
}
```

Figures shown are from a September 2026 run over the June 2026 Amsterdam
snapshot and the Austin live estimate, and move as the markets move.

### Use cases

**Rental arbitrage before a lease signature.** The operator has a target
neighbourhood and a rent number. The rollup gives the nightly rate
distribution and the revenue median with its denominator; the live tier gives
the estimate for the specific address and bedroom band. Both figures are on
one row with their dates, so the comparison is honest.

**Lender or insurer pricing short-let risk.** Market grain is what a credit
paper needs: how many listings, what mix, what the revenue distribution looks
like, and which months pay. The seasonality curve is the part a spreadsheet
built from listing rows never produces cleanly.

**Private equity market sizing.** Twenty markets in one run, one row each.
`max_markets` bounds the run and the per-market charge means a pilot of three
markets costs a pilot's money. The neighbourhood mix says where the market
actually concentrates.

**A platform deciding which city to launch.** Booked share by month answers
whether a city is a year-round market or a summer one, and the room mix says
whether the inventory is whole homes or rooms. Both come from the published
snapshot rather than from a live scrape, which is the point when the decision
is about the market and not about one listing.

### How it compares

| | This actor | xtracto/airbnb-occupancy | malikgen/airbnb-revenue-calculator | AirDNA MarketMinder |
|---|---|---|---|---|
| Output grain | Market rollup | Per listing / one estimate | One estimate | Market |
| Per 1k rows | $50.00 | Free (compute only) | Free (compute only) | Subscription |
| Revenue denominators named | Yes, all three | No | No | No |
| Median headline | Yes | No | No | Yes |
| Seasonality curve | Yes | No | No | Yes |
| Live estimate in the same call | Yes | No | Yes | Behind a login |
| Aggregates only, attributed | Yes | No | No | Yes |
| Snapshot date on the row | Yes | No | No | Yes |

The closest competitor by scope is `xtracto/airbnb-occupancy` at 43 users,
free, returning occupancy figures rather than a rollup. The comparison a
buyer in this category actually makes is against AirDNA, which publishes the
same class of number behind a subscription and a signed-in session, and
against the free calculators that return one estimate and an email funnel.
What those have that we do not: a live per-listing rentaliser on demand, and
AirDNA's longer historical series. What this Actor has that they do not: the
three revenue means with their denominators on the row, a seasonality curve,
and a data product rather than a lead form.

### Pricing

All pricing is pay-per-event at $0.05 per `market`, one event per returned
market rollup row. $50.00 per 1,000 markets. Rows that carry no figures at
all are delivered but never charged. All pricing is pay-per-event, you only
pay for results you receive. No actor-start fee, no per-compute-unit charges.

### Limits and gotchas

- The published snapshot is taken on a schedule and carries its own date. The
  row reports it as `snapshot_as_of`. A market published in June will show
  June numbers in October. That is the data, not a bug.
- Aggregates only. The data licence allows derived statistics with
  attribution and the snapshot date and does not allow redistributing the
  listings, so no listing identifier, per-listing price or coordinate appears
  in the output. If you need listing rows, you need a different product.
- The revenue column's three means are three different questions. Quote the
  median and its denominator. Do not quote a bare mean and do not average the
  three together.
- The live tier covers US addresses only and is asked once per run, no matter
  how many markets you list. It answers one address at a time and a burst
  earns a refusal, so the Actor runs it sequentially and reports a refusal as
  `status: "blocked"` rather than retrying into a block.
- The live tier's comparables are filtered by `min_bedrooms` and
  `max_bedrooms`. The defaults are 1 and 5. A studio-only market needs them
  lowered.
- The two indexes are never averaged. `indexes_are_never_averaged` is true on
  every row, and the two blocks carry their own dates. Blending a snapshot
  median with a live average is a decision for you to make deliberately.
- A market pulls two compressed files of several megabytes each and
  aggregates a few million calendar rows. One market takes tens of seconds;
  twenty takes several minutes.

### FAQ

**How much can I make on an Airbnb in a given market?**
The rollup gives the market's revenue median with the count it was taken
over, and the live tier gives an estimate for a specific US address with 25th
and 75th percentile bands. Quote the median, not the mean: on a real market
the three ways of averaging the revenue column differ by a factor of two.

**What does the Airbnb revenue estimator return that a calculator does not?**
A calculator returns one number for one address. This Actor returns the
market around it: the rate distribution, the occupancy days, the revenue
denominators, the room and bedroom mix, and the months that pay. The live
estimate is one block of that row, not the whole answer.

**Is there a seasonality view?**
Yes. `seasonality.months` carries listed days and booked share per month,
aggregated over every listing's calendar. It is the fastest way to see whether
a market is year-round or seasonal.

**Do I get per-listing rows?**
No. The output is aggregates with attribution and the snapshot date, which is
what the data licence allows. Per-listing rows would be a different product
and a different licence.

**Can I price a short-term rental market outside the US?**
Yes, for the snapshot rollup. The live estimate tier is US addresses only.
Amsterdam, Lisbon, Barcelona and 120 other markets have published snapshots.

**Why are there three different revenue figures in the row?**
Because the column has three honest averages and they differ by a factor of
two. Each is reported with the count it was taken over so a reader knows which
question it answers. The headline is the median.

### Related Actors

- [Cost of Living Actor](https://apify.com/s-r/cost-of-living) for the cost
  side of a market next to its rental revenue.
- [Zillow Scraper](https://apify.com/s-r/zillow-scraper) for US rental
  listings and long-let pricing beside short-let revenue.
- [Apartments Scraper](https://apify.com/s-r/apartments-scraper) for
  long-let inventory next to a short-let market rollup.

# Actor input Schema

## `markets` (type: `array`):

Markets to roll up. City name or `city, country` to disambiguate. Up to 20 per run.

## `address` (type: `string`):

A US street address to price live. The live tier is asked once per run regardless of how many markets you list. Leave empty for the snapshot rollup alone.

## `include_snapshot` (type: `boolean`):

Aggregate the published market snapshot: rate, occupancy, revenue, room and bedroom mix, neighbourhood mix. On by default.

## `include_seasonality` (type: `boolean`):

Month-by-month booked share from the published calendar snapshot. On by default.

## `include_live` (type: `boolean`):

Live estimate for the address above. US addresses only. On by default; skipped when no address is given.

## `min_bedrooms` (type: `number`):

Narrow the live tier's comparables by bedroom count. Default 1.

## `max_bedrooms` (type: `number`):

Upper bound on bedrooms for the live tier's comparables. Default 5.

## `max_markets` (type: `number`):

Upper bound on markets returned per run, 1 to 20. Default 5.

## Actor input object example

```json
{
  "markets": [
    "Amsterdam",
    "Austin",
    "Lisbon, Portugal"
  ],
  "address": "200 E 6th St, Austin, TX 78701",
  "include_snapshot": true,
  "include_seasonality": true,
  "include_live": true,
  "min_bedrooms": 1,
  "max_bedrooms": 5,
  "max_markets": 5
}
```

# Actor output Schema

## `results` (type: `string`):

One row per market with the snapshot rollup, the seasonality curve, the live estimate where asked for, grain labels, attribution and coverage counts.

## `output` (type: `string`):

OUTPUT record with the run's counts, coverage totals and status flags.

## `errors` (type: `string`):

Failures with a code and a redacted message. Absent when the run had none.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "markets": [
        "Amsterdam"
    ],
    "address": "200 E 6th St, Austin, TX 78701",
    "min_bedrooms": 1,
    "max_bedrooms": 5
};

// Run the Actor and wait for it to finish
const run = await client.actor("s-r/str-market-sample").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "markets": ["Amsterdam"],
    "address": "200 E 6th St, Austin, TX 78701",
    "min_bedrooms": 1,
    "max_bedrooms": 5,
}

# Run the Actor and wait for it to finish
run = client.actor("s-r/str-market-sample").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "markets": [
    "Amsterdam"
  ],
  "address": "200 E 6th St, Austin, TX 78701",
  "min_bedrooms": 1,
  "max_bedrooms": 5
}' |
apify call s-r/str-market-sample --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,s-r/str-market-sample"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/9HEprpYOG4lg8Fz5Q/builds/NTxw0oCKkWRJBg2aM/openapi.json
