Markets
Real-time competitive intelligence for every short-term-rental market you operate in. Powered by Atlas — comp sets, nightly rates, availability, occupancy, demand signals, and local events, exposed through one read API. Learn more about Atlas →
What you get
- Markets you operate in — auto-derived from your listings. As soon as you add a property in Lisbon, Lisbon shows up in
GET /v1/markets. - Market-wide metrics — active listing count, median ADR, occupancy, revenue per available night, supply trend, and a 90-day demand index.
- Per-listing comp sets — the same competitive set used to price your listing, returned with rate position (p25 / p50 / p75) and an under/over-priced flag.
- Calendar overlays — day-by-day demand from Wheelhouse plus a curated local-events feed (concerts, sports, conferences) with rank and expected attendance.
- Browse new markets — a global slice you can paginate through to find markets to expand into, sorted by yield, growth, or supply.
Where this data comes from
List your markets
Returns every market that contains at least one of your listings, plus a top-line snapshot of each. This is the right call to drive a markets index page in your dashboard.
/v1/marketscurl 'https://api.repull.dev/v1/markets' \ -H 'Authorization: Bearer sk_live_...'
Query parameters
limitintegerDefault: 50Max markets to return. 1-200.
offsetintegerDefault: 0Pagination offset.
sortstringDefault: listings_descOne of listings_desc, adr_desc, occupancy_desc, revpan_desc.
Response
{
"data": [
{
"city": "lisbon",
"display_name": "Lisbon, Portugal",
"country": "PT",
"your_listings": 14,
"metrics": {
"active_listings": 8421,
"median_adr": 142.50,
"occupancy": 0.71,
"revpan": 101.18,
"supply_trend_30d": 0.04,
"demand_index_90d": 1.18
},
"updated_at": "2026-04-30T22:14:09.000Z"
},
{
"city": "barcelona",
"display_name": "Barcelona, Spain",
"country": "ES",
"your_listings": 7,
"metrics": {
"active_listings": 11293,
"median_adr": 168.00,
"occupancy": 0.79,
"revpan": 132.72,
"supply_trend_30d": -0.01,
"demand_index_90d": 1.04
},
"updated_at": "2026-04-30T22:14:09.000Z"
}
],
"has_more": false
}Market deep dive
Drop into a single market for the full picture: percentile rate distribution, channel mix, amenity-weighted comp sets, top-performing listings, and the trailing 90-day occupancy and ADR series. Use this to back a single-market dashboard or a deep analysis surface.
/v1/markets/{city}curl 'https://api.repull.dev/v1/markets/lisbon' \ -H 'Authorization: Bearer sk_live_...'
Response (excerpt)
{
"city": "lisbon",
"display_name": "Lisbon, Portugal",
"country": "PT",
"metrics": {
"active_listings": 8421,
"median_adr": 142.50,
"occupancy": 0.71,
"revpan": 101.18
},
"rate_distribution": {
"p10": 68,
"p25": 92,
"p50": 142.50,
"p75": 198,
"p90": 285
},
"channel_mix": {
"airbnb": 0.62,
"booking": 0.31,
"vrbo": 0.05,
"direct": 0.02
},
"your_position": {
"listing_count": 14,
"median_adr": 178,
"rate_position": 0.68,
"underpriced_count": 2,
"overpriced_count": 0
},
"trailing_90d": {
"adr": [/* ... daily series ... */],
"occupancy": [/* ... daily series ... */]
},
"updated_at": "2026-04-30T22:14:09.000Z"
}Calendar view
Day-by-day demand and pricing across a date range. Combines Wheelhouse market demand with a curated events feed — concerts, sports, conferences, and major holidays — so you can see why a Friday three weeks from now is going to spike.
/v1/markets/{city}/calendarcurl 'https://api.repull.dev/v1/markets/lisbon/calendar?from=2026-06-01&to=2026-06-30' \ -H 'Authorization: Bearer sk_live_...'
Query parameters
fromstring (YYYY-MM-DD)RequiredFirst date in the range. Inclusive.
tostring (YYYY-MM-DD)RequiredLast date in the range. Inclusive. Max 365 days from `from`.
listing_idstringOptional. Overlay your listing's current rates and bookings on top of the market view.
Response (excerpt)
{
"city": "lisbon",
"from": "2026-06-01",
"to": "2026-06-30",
"days": [
{
"date": "2026-06-01",
"demand_index": 0.94,
"median_adr": 138,
"occupancy": 0.69,
"events": []
},
{
"date": "2026-06-12",
"demand_index": 1.74,
"median_adr": 224,
"occupancy": 0.92,
"events": [
{
"name": "Rock in Rio Lisboa — Day 1",
"category": "concert",
"rank": 1,
"expected_attendance": 80000
}
]
},
{
"date": "2026-06-13",
"demand_index": 1.81,
"median_adr": 238,
"occupancy": 0.95,
"events": [
{
"name": "Rock in Rio Lisboa — Day 2",
"category": "concert",
"rank": 1,
"expected_attendance": 80000
},
{
"name": "Festas de Lisboa — Santo António",
"category": "festival",
"rank": 2,
"expected_attendance": 250000
}
]
}
]
}Pass listing_id to see your overlay
?listing_id=lst_abc123 and each day in the response also returns your_rate, your_booked, and recommended_rateso you can render "you vs the market" in one chart.Common patterns
Show me where I'm under-priced
your_position.underpriced_count on each market tells you how many of your listings are sitting below the comp-set median. Sort markets by it to triage.
const { data } = await repull.markets.list()
const opportunities = data
.filter(m => m.your_position && m.your_position.underpriced_count > 0)
.sort((a, b) => b.your_position.underpriced_count - a.your_position.underpriced_count)
for (const m of opportunities) {
console.log(`${m.display_name}: ${m.your_position.underpriced_count} under-priced`)
}Find new markets to expand into
Pass browseMarkets: trueto escape the "markets I'm already in" scope and see the global ranked list. Sort by RevPAN, occupancy, or growth.
const { data } = await repull.markets.list({
browseMarkets: true,
sort: 'revpan_desc',
limit: 25,
})
// data is now ranked across every market in the catalog, not just yours.Heat map of my listings vs market
For each of your listings, fetch the calendar with the listing overlay. The diff between your_rate and recommended_rate across 30 days gives you a per-day heat-map cell.
const listings = await repull.properties.list()
const heatmap = await Promise.all(
listings.data.map(l =>
repull.markets.calendar(l.market, {
from: '2026-06-01',
to: '2026-06-30',
listing_id: l.id,
})
)
)
// Render heatmap[i].days[j].your_rate vs heatmap[i].days[j].recommended_rate
// in a grid (listings on Y, dates on X).Caching
Shared market slices (the data that's the same for everyone in a market) are cached for 5 minutes. Per-listing overlays — anything that depends on your account or a specific listing ID — bypass the cache and fetch live. The response always includes updated_at so you know how fresh the data is.
Rate limits
Markets endpoints follow the standard tier-based limits. See Rate Limits for the per-tier numbers. The deep-dive and calendar endpoints are higher cost than GET /v1/markets — budget accordingly.