Market Insights¶
GET /api/v1/listings/<id>/market-insights/
- Scope:
insights:read - Description: compare one listing's daily performance against the benchmark for its neighborhood or its whole market.
Try it out
Explore parameters, schemas, and live requests in the Swagger UI.
What you get¶
One entry per stay date, each carrying both sides of the comparison:
listing— this listing's own performance, from its reservations and calendar.benchmark— the comparable-property benchmark for the cohort this listing is compared against, built from publicly available data.
This is the same pair of series the Market Data tab charts inside the Beyond app.
The comparison is anchored to a listing you already have access to, and the
cohort is always derived from that listing — you choose its grain with
filter[compare-to], never the cohort itself.
The benchmark¶
The benchmark side is not a whole-city average. It is the listing's own
cohort, narrowed to the listing's bedroom count and to entire-home
rentals — a comparable-grade benchmark rather than a coarse area-wide figure.
| Derived from | Behavior |
|---|---|
| Neighborhood or market | The listing's own, per filter[compare-to]. A listing assigned to neither is a 422. |
| Bedrooms | The listing's own bedroom count, echoed as meta.cohort-bedrooms. A listing with no bedroom count set compares against all bedroom counts. |
| Currency | The listing owner's billing currency, echoed as meta.currency. |
For a multi-unit parent listing, the listing side aggregates the parent's
sub-units — the parent holds no reservations itself, so it would otherwise read
as 0% occupied.
Comparison scope¶
filter[compare-to] selects how wide the benchmark cohort is drawn. Both
values are derived from the listing — neither lets you name an area you have no
listing in.
| Value | Cohort |
|---|---|
cluster (default) |
The listing's neighborhood — the Beyond-defined area it sits in. This is what the in-app Market Data tab charts. |
market |
The listing's whole market, typically a city or region. Coarser, but populated where a neighborhood is too thin to measure. |
Reach for market when the default returns
meta.benchmark-data-available: false, or when a listing is assigned to a
market but not to a neighborhood — that case is a 422 under cluster and
answers normally under market.
Because the two produce different numbers for the same date, compare-to is
part of an entry's id and is echoed as meta.compare-to.
Request¶
GET /api/v1/listings/123/market-insights/
?filter[start-date]=2026-01-01
&filter[end-date]=2026-06-30
&page[size]=366
| Parameter | Description |
|---|---|
filter[start-date] |
First stay date (YYYY-MM-DD). Defaults to 1 January of the current year. |
filter[end-date] |
Last stay date (YYYY-MM-DD). Defaults to 31 December of the current year. The range must not exceed 731 days. |
filter[compare-to] |
Which cohort to benchmark against: cluster (default) or market. See Comparison scope. Any other value is a 400. |
page[number], page[size] |
Standard pagination. Default page size 366, maximum 731. |
One entry is returned for every day in the requested window, whether or not either side has data for it, so the response length is a function of the request alone. The default window is the current calendar year, which is one page at the default page size in every year, leap years included — omit both date filters and a single request gives you the whole year, history and forward book together.
Stricter rate limit
This is a heavy endpoint: each call runs a full analytics scan. It carries a stricter limit of 30 requests/minute, on top of the application-level limit that applies to every endpoint. Both counters are scoped the same way: per Beyond user if you authenticate with a personal access token, per application if you are a partner integration. Exceeding it returns 429 — cache responses and avoid tight polling loops. See Rate Limiting.
Response¶
{
"data": [
{
"type": "market-insights",
"id": "123:cluster:2026-03-01",
"attributes": {
"date": "2026-03-01",
"adj-occupancy": {"listing": 71.4, "benchmark": 63.0},
"occupancy": {"listing": 68.2, "benchmark": 59.0},
"average-booked-rate": {"listing": 210.5, "benchmark": 188.0},
"average-posted-rate": {"listing": 225.0, "benchmark": 191.0}
}
},
{
"type": "market-insights",
"id": "123:cluster:2026-08-20",
"attributes": {
"date": "2026-08-20",
"adj-occupancy": {"listing": 0.0, "benchmark": 58.0},
"occupancy": {"listing": 0.0, "benchmark": 55.0},
"average-booked-rate": {"listing": null, "benchmark": 175.0},
"average-posted-rate": {"listing": 240.0, "benchmark": 179.0}
}
}
],
"meta": {
"pagination": {"page": 1, "pages": 1, "count": 2},
"currency": "GBP",
"benchmark-data-available": true,
"compare-to": "cluster",
"cohort-bedrooms": ["2"],
"start-date": "2026-01-01",
"end-date": "2026-12-31"
}
}
Entry ids¶
An entry's id is {listing-id}:{compare-to}:{stay-date} — for example
123:cluster:2026-03-01.
The stay date alone would not identify it. The benchmark half of a row depends
on which listing you asked about and on which cohort you compared it to, so
the same date carries different numbers from one listing to the next and from
cluster to market. Folding both in keeps ids unique the way
JSON:API expects, which matters if your client caches or
de-duplicates resources by type and id.
Ids are stable: the same request always returns the same id for the same date.
Metrics¶
Every metric is an object with the same two keys, listing and benchmark.
| Field | Description |
|---|---|
date |
The stay date this entry describes. |
adj-occupancy |
Adjusted occupancy, as a percentage: booked nights over available nights. Nights that were blocked or otherwise unavailable do not count against it. |
occupancy |
Occupancy over all nights, including nights that were never available to book. Lower than adj-occupancy whenever there is blocked inventory. |
average-booked-rate |
Average nightly rate actually booked, in meta.currency. null on a date with no bookings. |
average-posted-rate |
Average nightly rate on offer, booked or not, in meta.currency. Use it where average-booked-rate is null. |
These names match the ones comp sets uses for the same quantities, so the two endpoints can be read with one vocabulary.
Rates are in major currency units — 210.5 means 210.50 in
meta.currency, not 210 cents. The calendar endpoint uses the
same major units, but as whole-number integers in the listing's own currency.
null never means zero¶
This is the most important thing to get right when charting the response.
0.0is a real measurement: the listing was available and nothing booked.nullmeans the metric could not be computed for that date — no availability, no bookings, or no benchmark coverage.
The second entry in the example above is the case worth reading twice. On
2026-08-20 the listing was available and unbooked, so adj-occupancy.listing
is a genuine 0.0, average-booked-rate.listing is null (no bookings,
therefore no booked rate), and average-posted-rate.listing is 240.0 — it was
on offer at 240 and nobody took it.
Plot null as a gap, not as zero. Averaging null as 0 will understate rates
on every unbooked date. If you want the app's behavior — a continuous line —
fall back explicitly: average-booked-rate → average-posted-rate for prices,
adj-occupancy → occupancy for occupancy. Both sides of each pair are in the
response precisely so that the choice is yours rather than ours.
Document meta¶
| Field | Description |
|---|---|
currency |
ISO currency code every rate in the response is expressed in. Derived, see below. |
benchmark-data-available |
false when there is no benchmark coverage at all for the cohort in the requested window. The benchmark side is null throughout; the listing side is still fully populated. Widening with filter[compare-to]=market often recovers coverage. |
compare-to |
The cohort grain actually applied, cluster or market. Echoed so you can tell a defaulted request from an explicit one. See Comparison scope. |
cohort-bedrooms |
The bedroom counts the benchmark was narrowed to, as a list — currently always the listing's own, e.g. ["2"]. null when the listing has no bedroom count set, meaning every bedroom count was included. It is a set of exact counts, not a maximum. |
start-date, end-date |
The stay-date window actually applied, after defaulting. Read these rather than assuming, especially when you omit the date filters. |
pagination |
Standard pagination metadata. |
currency is derived, not requested
There is no currency parameter. Every rate is returned in the currency from
the listing owner's billing configuration, and meta.currency names it
on every response.
That is not the listing's own currency — the one listings
reports and calendar prices are quoted in. The two are set
independently and can differ, so convert before comparing a rate from this
endpoint with a price from another one.
Read meta.currency on every response rather than caching it per listing:
it is the only signal you get, and it changes when the billing
configuration does.
Use cases¶
- Occupancy pacing. Chart
adj-occupancy.listingagainstadj-occupancy.benchmarkover the forward window to see where the listing is filling faster or slower than its cohort — the earliest signal that pricing is off for a date range. - Rate positioning. Compare
average-booked-rate.listingwithaverage-booked-rate.benchmarkto find dates where the listing books below what comparable properties achieve. - Distinguishing soft pricing from soft demand. Occupancy at or above the benchmark while the booked rate trails it means the listing is underpriced; both below means demand is the constraint.
- Ask vs. achieved. The gap between
average-posted-rateandaverage-booked-rateon the same side shows how much of the asking price guests actually pay. - Blocked-inventory audit. A wide, persistent gap between
adj-occupancyandoccupancyon the listing side quantifies revenue lost to blocked nights rather than to weak demand. - Portfolio scan. Sweep the endpoint across a portfolio and rank listings by their gap to their own cohort — each is benchmarked against its own surroundings, so the comparison holds across locations. Mind the 30/minute limit and spread the sweep.
Errors¶
| Status | Cause |
|---|---|
400 |
Malformed filter[start-date] / filter[end-date], end before start, or a range longer than 731 days. |
401 |
Missing or invalid bearer token. |
403 |
Token does not have the insights:read scope. |
404 |
No listing with that id is visible to your application — it does not exist, is not owned by your application, was deleted, or your credential has no access to it. |
422 |
The listing cannot be matched to a cohort, or the benchmark query failed. See below. |
429 |
Rate limit exceeded — this endpoint is capped at 30 requests/minute, counted per Beyond user for personal access tokens and per application for partner integrations. See Rate Limiting. |
The two 422 cases are distinguished by the error's code:
code |
Meaning |
|---|---|
listing_not_analysable |
The listing is not assigned to a cohort, so there is nothing to compare it against. A listing with a market but no neighborhood hits this only under the default filter[compare-to]=cluster — retry with market and it answers. A listing with no market at all is not retryable; the assignment has to change first. |
market_insights_unavailable |
The benchmark query failed. Transient — retry with backoff. |
{
"errors": [
{
"status": "422",
"detail": "Listing 123 is not assigned to a market, so it has no neighborhood to compare against.",
"source": {"pointer": "/data"},
"code": "listing_not_analysable"
}
]
}
An empty benchmark is not an error: it is a 200 with
meta.benchmark-data-available: false. Your listing's own series is still there.