# Airbnb Host Portfolio & Operator Mapper (`apt_marble/airbnb-host-portfolio-mapper`) Actor

Turn Airbnb listings into host entities. One row per operator: every listing found in the areas you scan, whole-property share, years hosting, registration numbers and an auditable 0-100 operator score. Filter to multi-property hosts, scan a city as a grid, get a coverage report. No account needed.

- **URL**: https://apify.com/apt\_marble/airbnb-host-portfolio-mapper.md
- **Developed by:** [Hamza](https://apify.com/apt_marble) (community)
- **Categories:** Travel, Real estate, Automation
- **Stats:** 1 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.80 / 1,000 row saveds

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Airbnb Host Portfolio & Operator Mapper

Airbnb shows you listings. This actor shows you the **people and companies behind
them**. Point it at a city or a map rectangle and it comes back with one row per
host: every listing of theirs it found, how many are whole properties, how long
they have been hosting, their own rating, the registration or licence number on
each listing and where that number was published, the geographic centre and
spread of their portfolio, and a published 0–100 commercial-operator score. It is
built for regulators, housing researchers, hotel and short-let analysts, and
anyone who needs to separate the neighbour renting a spare room from an operator
running forty flats. No Airbnb account, no login, nothing to connect.

### What you can do with it

- **Find the commercial operators in a market.** Set a minimum portfolio size of
  two or more and the dataset becomes a list of multi-property hosts, ranked by
  size and by operator score.
- **Support a short-let compliance review.** Every listing reports its
  registration or licence number and which of the places Airbnb publishes it the
  number came from, plus an optional check against the format your city uses.
- **Measure real market concentration.** Whole-property share, portfolio size
  distribution and median nightly rate per host tell you how much of a
  neighbourhood's supply sits with a handful of operators.
- **Build a property-management prospect list.** Hosts with several whole
  properties, a real tenure and no superhost badge are exactly the audience for
  management, cleaning, insurance and pricing tools.
- **Map a competitor's footprint.** The portfolio centroid, spread in kilometres
  and neighbourhood list show where an operator actually concentrates.
- **Track a market over time.** Run it on a schedule and compare host rows
  between runs to see who is growing, who has left and who has newly appeared.

### What you get

One row per host. Abridged example:

```json
{
  "recordType": "host",
  "hostUserId": "10214195",
  "hostName": "Pierre",
  "hostProfileUrl": "https://www.airbnb.com/users/profile/1462834525352805037",
  "hostContextualUserId": "1462834525352805037",
  "isSuperhost": false,
  "isVerified": true,
  "hostTitleText": "Host",
  "yearsHosting": 11,
  "monthsHosting": 133,
  "hostRatingAverage": 4.68,
  "hostRatingCount": 79,
  "listingCount": 2,
  "listingCountIsComplete": false,
  "listingCountScope": "Minimum — listings found in the areas scanned (Paris, France); the host may hold more",
  "listingCountStatement": "2 listings found for this host in the areas this run scanned (Paris, France). That is a minimum, not a total: the scan stopped at the listing limit set for the run, so this host may hold more listings in the same areas. Listings this host holds outside the areas scanned are not counted, and Airbnb does not publish a host's total listing count anywhere on a listing page, so no tool can report one. Scan a wider area or raise the listing limit to raise this floor.",
  "placesSeenIn": ["Paris, France"],
  "coverageConfidence": "partial",
  "scanStoppedAtListingLimit": true,
  "scanStoppedAtRunLimit": false,
  "areasLeftCrowded": 0,
  "portfolioMetricsArePartial": true,
  "listingIds": ["1737062443587734735", "1716681499327714304"],
  "entireHomeCount": 2,
  "entireHomeSharePct": 100,
  "privateRoomCount": 0,
  "medianNightlyRate": 217.2,
  "currency": "USD",
  "totalPersonCapacity": 8,
  "medianBedrooms": 1,
  "listingsWithRegistration": 2,
  "listingsWithoutRegistration": 0,
  "registrationNumbers": ["7510203094826"],
  "licenceSources": ["descriptionModal"],
  "registrationFormatValidCount": 1,
  "centroidLatitude": 48.87051,
  "centroidLongitude": 2.34879,
  "coordinateSpreadKm": 4.311,
  "neighbourhoods": ["Paris, Île-de-France, France"],
  "commercialOperatorScore": 47,
  "commercialOperatorScoreIsProvisional": true,
  "commercialOperatorVerdict": null,
  "commercialOperatorVerdictBasis": "No verdict: the scan covered part of the area, so a larger portfolio cannot be ruled out and calling this host small would be a guess. The score of 47 out of 100 describes only the listings found here.",
  "commercialOperatorScoreInputs": {
    "listingCount": 2,
    "entireHomeCount": 2,
    "entireHomeSharePct": 100,
    "yearsHosting": 11,
    "portfolioSizePoints": 6.67,
    "entireHomePoints": 25,
    "tenurePoints": 15
  },
  "placesScanned": ["Paris, France"],
  "scrapedAt": "2026-07-29T10:00:00.000Z"
}
```

Alongside the host rows you also get, unless you switch them off, one
`recordType: "listing"` row per property — with its own `registrationNumber`,
`licenceSource`, `propertyType`, `bedrooms`, `beds`, `bathrooms`,
`personCapacity`, `ratingValue`, `reviewCount`, coordinates and the host it
belongs to — and one `recordType: "coverageReport"` row per area scanned, which
states in plain English how much of that area was actually covered.

### Input reference

| Field | Type | Default | What it does |
| --- | --- | --- | --- |
| `places` | list of strings | `["Paris, France"]` | Cities or areas to scan, written the way you would type them into Airbnb's own search box. Up to 10. |
| `bboxes` | list of objects | `[]` | Optional exact rectangles, each `{neLat, neLng, swLat, swLng}` with an optional `query` naming the neighbourhood. Up to 20. |
| `tileSaturationThreshold` | integer | `240` | When one map area reaches this many listings it is divided into four smaller areas and each is scanned again. Lower is more thorough and slower. Max 320. |
| `maxQuadtreeDepth` | integer | `3` | How many times a crowded area may be divided. Max 6. |
| `roomTypes` | multi-select | `[]` | Restrict to `Entire home/apt`, `Private room`, `Shared room` or `Hotel room`. Whole properties are what short-let rules usually target. |
| `priceBands` | list of objects | `[]` | Optional `{min, max}` slices, used only when an area is still crowded after being divided as far as allowed. Up to 8. |
| `minPortfolioSize` | integer | `1` | Only save hosts with at least this many listings in the area scanned. Set 2+ for multi-property operators only. Max 50. |
| `unregisteredOnly` | boolean | `false` | Save only hosts with at least one listing whose registration number is not visible. |
| `registrationFormat` | string | empty | Optional pattern each number is checked against, e.g. `^\d{13}$`. Each record then reports whether it matched. |
| `includeLicence` | boolean | `true` | Collect registration and licence numbers. |
| `emitListingRows` | boolean | `true` | Also save one row per listing behind each portfolio. Turning it off lowers both the row count and the cost. |
| `maxListings` | integer | `500` | The main size and cost control: how many listings get looked up individually to find their host. It also decides how much of an area you see — see the note below. Max 5,000. |
| `maxRequests` | integer | `800` | A hard ceiling on total work per run. The run stops cleanly when it is reached and the coverage report says so. Max 8,000. |
| Parallel workers | integer | `4` | How many listings are worked through at the same time. Higher finishes sooner; 4 is a good balance. Max 8. |
| `currency` | string | `USD` | Currency for the nightly rates in the output. |
| `locale` | string | `en` | Language for names, property types and neighbourhood labels. |
| `proxyCountry` | string | empty | Country to appear to browse from, as a two-letter code. Leave empty unless a market looks different from different countries. |

> **What the default 500 buys, and when to change it.** 500 is a cheap first look
> — enough to scan a single district until it stops producing new listings.
> A whole city is a different job: Paris
> alone carries tens of thousands of listings, so a 500-listing run over it is a
> thin slice, and in a thin slice **almost every host looks like a one-property
> host** — a real operator with thirty flats across the city only shows the
> handful that happened to fall inside the slice. That is a fact about how much
> was scanned, not about the market, and the run says so in the log, in the
> coverage report and on every host row.
>
> The default is deliberately left small, because it is also what you are billed
> for and raising it does not on its own make a city complete: even the 5,000
> maximum is a fraction of Paris. **If you want portfolios rather than a sample,
> scan an area small enough to finish** — one or two map rectangles over the
> districts you care about — and the run can then tell you a host is small,
> instead of withholding the verdict. **If you want a broad market sweep**, raise
> `maxListings` and read every count as the floor it is labelled as.

### Output fields

#### Host rows (`recordType: "host"`)

| Field | Type | Description |
| --- | --- | --- |
| `hostUserId` | string | Airbnb's own identifier for the host. Stable between runs, so it is the key to join on. |
| `hostName` / `hostTitleText` | string | Display name, and the label Airbnb shows under it (`Host`, `Superhost`). |
| `hostProfileUrl` / `hostContextualUserId` | string | Link to the public profile, and the separate identifier that link is built from. |
| `isSuperhost` / `isVerified` | boolean | Badge and identity-verification status. |
| `yearsHosting` / `monthsHosting` | number | Tenure, in whole years and in total months. |
| `hostRatingAverage` / `hostRatingCount` | number | The host's own rating and review total across all their listings. |
| `listingCount` / `listingIds` | number / array | **How many of this host's listings the run found in the areas it scanned, and their identifiers. It is a floor, not a verified total** — see the note under this table. |
| `listingCountIsComplete` | boolean | `true` only when every area this host appeared in was scanned until it stopped producing new listings. `false` means the count is a minimum. |
| `listingCountScope` | string | One line naming exactly what the count covers, so a column of numbers is never read on its own. |
| `listingCountStatement` | string | The same thing in full sentences: what was counted, why it may be short, and how to raise it. |
| `placesSeenIn` / `coverageConfidence` | array / string | Which of the areas you asked for this host turned up in, and how well those areas were covered (`high`, `partial`, `low`, `unknown`). |
| `scanStoppedAtListingLimit` / `scanStoppedAtRunLimit` / `areasLeftCrowded` | boolean / boolean / number | Whether your listing limit or the run's own size limit ended the scan early, and how many areas were still producing new listings when it stopped. Any of these means the count is short. |
| `portfolioMetricsArePartial` / `metricsAffectedByScanScope` | boolean / array | Whether the portfolio numbers on this row are bounded by the scan, and the exact list of columns that are. |
| `entireHomeCount` / `entireHomeSharePct` / `privateRoomCount` | number | How much of the portfolio **found in the areas scanned** is a whole property versus a room. |
| `medianNightlyRate` / `currency` | number / string | Median nightly rate observed across the portfolio, in your chosen currency. |
| `totalPersonCapacity` / `medianBedrooms` | number | Combined guest capacity and typical property size, across the listings found. |
| `listingsWithRegistration` / `listingsWithoutRegistration` | number | How many of the listings found publish a registration number. Both are counts of what was found, so both move if a wider area is scanned. |
| `registrationNumbers` / `licenceSources` | array | The distinct numbers found, and where each was published. |
| `registrationFormatValidCount` | number | How many matched your expected format. Null when no format was given. |
| `centroidLatitude` / `centroidLongitude` / `coordinateSpreadKm` | number | Geographic centre of the portfolio and the distance between its two furthest listings. |
| `neighbourhoods` | array | Neighbourhood labels the portfolio spans. |
| `commercialOperatorScore` | number | 0–100 composite, see below. Built from the listings found, so it moves as the scan widens. |
| `commercialOperatorScoreIsProvisional` | boolean | `true` when the score was computed from a portfolio that was only partly found. |
| `commercialOperatorVerdict` | string | `likely commercial operator`, `small or individual host`, or **blank**. It is left blank rather than guessed whenever the areas were not scanned in full and the listings found are too few to be sure — calling a large operator a small host is the one mistake this dataset refuses to make. |
| `commercialOperatorVerdictBasis` | string | Why the verdict says what it says, in one sentence. |
| `commercialOperatorScoreInputs` | object | Every number that produced the score, so it can be recomputed by hand. |
| `placesScanned` | array | Which of your inputs this run covered. |
| `scrapedAt` / `actorRunId` | string | When the row was produced, and by which run. |

> **About `listingCount`.** It counts the host's listings that this run found in
> the areas it scanned. It is a **floor, not a verified total**: the same host may
> hold more listings in the same city that the run did not reach, and more again
> in cities it never looked at. **Airbnb does not publish a host's total listing
> count anywhere on a listing page** — a listing page carries the host's rating,
> review count and how long they have been hosting, and nothing about how many
> properties they hold — so no tool can report a verified total, and any that
> claims to is counting what it happened to find. To raise the floor, scan a wider
> area (more cities, or map rectangles covering the neighbourhoods you care about)
> and raise `maxListings` so the sweep is not cut short. Sort by `listingCount`
> to rank operators, but read `listingCountIsComplete` and `listingCountScope`
> before quoting any single number.

#### Listing rows (`recordType: "listing"`)

`listingId`, `listingUrl`, `listingName`, `listingTitle`, `latitude`,
`longitude`, `propertyType`, `roomTypeText`, `personCapacity`, `bedrooms`,
`beds`, `bathrooms`, `ratingValue`, `reviewCount`, `isGuestFavorite`,
`nightlyRate`, `currency`, `neighbourhood`, `registrationNumber`,
`hasRegistration`, `licenceSource`, `registrationFormatValid`, `hostUserId`,
`hostName`, `isSuperhost`, `tileId`, `tileDepth`, `place`, `scrapedAt`.

#### Coverage rows (`recordType: "coverageReport"`)

`place`, `rootBbox`, `tilesVisited`, `tilesSplit`, `tilesStillSaturated`,
`uniqueListings`, `requestsUsed`, `budgetExhausted`, `listingCeilingReached`,
`maxDepthReached`, `mapAreasUnread`, `mapAreasPartlyRead`, `coverageConfidence`
(`high` / `partial` / `low`), `completenessStatement`, `listingsEnumerated`,
`listingsResolved`, `listingsAttributed`, `listingsWithoutHostCard`,
`listingsWithoutPdp`, `hostsFound`, `hostsBelowMinPortfolio`, `scrapedAt`.

`listingsResolved` counts the listings whose details were read;
`listingsAttributed` counts the ones that actually named a host, and
`listingsWithoutHostCard` counts the difference — listings that published no host
at all. Only the attributed ones can appear in a portfolio, so those three
numbers, plus `listingsWithoutPdp`, add up to `listingsEnumerated`.

`listingCeilingReached` is `true` when the scan stopped because it had collected
as many listings as you allowed, rather than because the areas ran out. When that
happens the confidence is never `high` and the plain-English statement says so —
a run that stops at your own limit has seen a slice of the market, not all of it.

### How the operator score is calculated

The score is a published formula, not a black box, and every input to it is in
the row so you can check the arithmetic:

| Component | Weight | How it is awarded |
| --- | --- | --- |
| Portfolio size | 60 points | 1 listing scores 0; 10 or more scores the full 60; linear in between. |
| Whole-property share | 25 points | 25 × the share of the portfolio that is a whole property rather than a room. |
| Tenure | 15 points | 15 × (years hosting ÷ 5), capped at 5 years. |

So a host with ten whole properties and five years of hosting scores 100, while a
long-standing host with a single whole flat scores 40 — the size of the portfolio
is deliberately the dominant term. It is a comparison aid, not an official
designation of any kind.

Because portfolio size dominates, the score moves with how much ground the run
covered. That is why each row carries `commercialOperatorScoreIsProvisional` and
why `commercialOperatorVerdict` is asymmetric: `likely commercial operator` is
given whenever the listings already found earn 50 points or more, since finding
more of a host's listings cannot make them smaller — but `small or individual
host` is only ever given when every area that host appeared in was scanned until
it stopped producing new listings. Anywhere else the verdict is left blank, with
the reason in `commercialOperatorVerdictBasis`. A blank verdict means "not
established", never "small".

### Pricing

This actor is **pay per event** and there are two charges:

- **Row saved** — $0.0008 for every row written, which is **$0.80 per 1,000
  rows**. It covers host rows, listing rows and coverage rows alike.
- **Listing host resolved** — $0.008 each time one listing is opened
  individually to read who hosts it and whether a registration number is
  published. This is the real work behind the dataset: it happens once per
  listing, and it is the reason `maxListings` is the setting that controls your
  bill.

A typical first pass — 500 listings resolved into roughly 300 host rows — costs
about **$4.24**, or about **$4.64** with the per-listing rows kept as well.
Filtering by minimum portfolio size or by missing registration reduces the rows
you keep but not the resolution charge, because a host can only be filtered once
their listings have been attributed to them. Areas that turn out to be empty cost
nothing, and if you set a maximum cost for a run the actor stops cleanly as soon
as that budget is spent rather than overshooting it.

### Limits and what this actor cannot do

Every one of these is real, and most of them are Airbnb's own ceilings rather
than this actor's:

- **`listingCount` is a floor, never a verified total.** It counts the host's
  listings this run found inside the areas it scanned. Airbnb does not publish a
  host's total listing count anywhere on a listing page — the page carries their
  rating, review count and how long they have been hosting, and nothing about how
  many properties they hold — so **no tool can report a verified total**, and one
  that claims to is reporting what it happened to find. Every host row therefore
  carries `listingCountIsComplete`, `listingCountScope` and the coverage grade for
  the areas that host was seen in, so the qualifier travels with the number even
  when you sort, filter or export. **To raise the floor: scan a wider area** (add
  cities, or map rectangles over the neighbourhoods you care about) **and raise
  `maxListings`** so the sweep is not cut short. A run of 500 listings over a city
  with tens of thousands will show most operators as one- or two-property hosts,
  because that is all it looked at.
- **Everything counted out of the portfolio inherits that floor** —
  whole-property share, guest capacity, how many listings lack a registration
  number, and the operator score itself. Each host row names those columns in
  `metricsAffectedByScanScope` and flags them with `portfolioMetricsArePartial`.
  Where the areas were not scanned in full, the row gives a score but withholds
  the one-word verdict rather than label a large operator a small host.
- **Airbnb publishes no index of a host's listings.** A portfolio can only be
  assembled by scanning an area and grouping what is found, so a host with
  properties outside the area you scanned is under-counted. This is the single
  most important limitation of this dataset, and it is why every run ships a
  coverage report.
- **Airbnb shows only a few hundred listings for any single search** and starts
  repeating itself past that point. A market is therefore covered by splitting it
  into map areas and merging the results, and **coverage cannot be proven
  complete** — the coverage report names the areas that were still crowded when
  scanning stopped and gives a confidence level of `high`, `partial` or `low`.
  Even `high` means "thorough for the area scanned", never "complete".
- **Registration and licence numbers are published inconsistently by market.**
  Each record reports where its number was found, and a blank means "not
  published anywhere we can see it" — **not** that a listing is unlicensed or
  that a host is breaking any rule. Treat the `unregisteredOnly` filter as a
  starting point for a review, never as a finding.
- **The host's own rating count is host-level.** It covers all of their listings
  and legitimately differs from any single listing's review count — 79 against 78
  on a real host we checked. Both numbers are in the output; do not compare them
  as if they measured the same thing.
- **Bedroom, bed and bathroom counts come from the listing's own summary line**,
  which is the only place Airbnb publishes them. A listing described only as a
  studio has no bedroom count, and that is reported as blank rather than guessed.
- **Nightly rates are what Airbnb was showing at the time of the run**, for its
  own default stay window and without any dates of yours. They are useful for
  comparing hosts within one run, not as a booked-revenue figure, and cleaning and
  service fees are not itemised separately.
- **The operator score is a composite with published weights**, not an official
  classification, a legal determination or a measure of anything Airbnb itself
  publishes. Because it is built from the listings found, it moves as the scan
  widens, and a low score on a partly scanned area means "not shown to be large
  here", never "small".
- **A host with no published host card cannot be attributed.** Those listings are
  counted separately in the run log and in each coverage row, and left out of the
  portfolios rather than assigned to a placeholder — which is why the listings
  looked up and the listings attributed to a host are two different numbers.
- **Only English has been verified.** Other languages will change how property
  types, neighbourhood labels and summary lines read, and the size parsing has
  not been checked against them.
- **Speed depends on the size of the job** and on how quickly Airbnb answers. No
  fixed run time is promised.
- **Airbnb's terms prohibit automated access.** You are responsible for using
  this data lawfully and in line with the source site's terms, and for whatever
  conclusions you draw from it.

### FAQ

**Do I need an Airbnb account?**
No. No Airbnb account, no login, no personal details — enter a city and run it.

**Is the data complete?**
No, and no tool can make it so. Airbnb never publishes a list of a host's
properties nor a count of them, and it stops showing new listings for any one
search after a few hundred. This actor gets past the second problem by scanning a
market as a grid of smaller areas, but the honest answer travels on every row:
`listingCount` is what was found in the areas scanned, `listingCountIsComplete`
says whether those areas were finished, and the coverage report grades each area
`high`, `partial` or `low`. Read those before quoting a number.

**Does a blank registration number mean the listing is illegal?**
No, and please do not report it that way. It means no number was published
anywhere visible for that listing. Some markets do not require one, some hosts
put it somewhere the public page does not show, and some listings are exempt.
Every record that does carry a number also says where it was found, which is what
makes the positives auditable.

**Can I run it on a schedule?**
Yes. Host rows are keyed on `hostUserId`, which is stable between runs, so
comparing two runs of the same area shows you which operators grew, shrank,
appeared or disappeared. Keep the place list and the size settings identical
between runs or the comparison will reflect your settings rather than the market.

**How do I get only the big operators?**
Set `minPortfolioSize` to 2 or more. Bear in mind that portfolio size is measured
inside the area you scanned, so widening the area usually raises it — a host with
three flats across a city looks like a one-listing host if you only scan one
district.

**Why do some hosts have fewer listings than I expected?**
Because the number is a floor. Either their remaining listings sit outside the
area scanned, or the run hit its `maxListings` ceiling before reaching them — the
row itself says which, in `listingCountScope`, `scanStoppedAtListingLimit` and
`areasLeftCrowded`. Airbnb publishes no total to compare against, so widening the
area and raising `maxListings` is the only way to raise the floor. The coverage
report's `listingsWithoutPdp` figure tells you how many listings were found but
never looked up, and `listingsWithoutHostCard` how many were looked up but
published no host at all.

**Can I get a full year of prices or availability per listing?**
Not from this actor — it is built around host identity. Use the companion
occupancy and price-tracking actors for that.

# Actor input Schema

## `places` (type: `array`):

Where to look for hosts. Type a place the way you would type it into Airbnb's search box, for example "Paris, France" or "Austin, Texas". Each place is scanned as a grid of map areas so that far more hosts are found than a single search would show.

## `bboxes` (type: `array`):

Optional. Scan a precise rectangle instead of a whole city. Each entry needs the north-east and south-west corners: {"neLat": 48.90, "neLng": 2.42, "swLat": 48.81, "swLng": 2.22}. You can add "query" to name the neighbourhood; if you leave it out, the first place above is used instead.

## `tileSaturationThreshold` (type: `integer`):

Airbnb stops showing new listings for any one area after a few hundred. When an area reaches this many listings, it is divided into four smaller areas and each is scanned again. Lower values mean more thorough coverage and a longer run.

## `maxQuadtreeDepth` (type: `integer`):

Each level divides a crowded area into four smaller ones. Every listing found costs a separate look-up here, so the default is deliberately shallow — raise it only together with the listing ceiling.

## `roomTypes` (type: `array`):

Restrict the scan to certain property types. "Entire home/apt" is the one short-let regulations usually target, so it is the most common choice for a compliance or market-share study. Leave empty for everything.

## `priceBands` (type: `array`):

Optional. Extra slices used only when an area is still crowded after being divided as far as allowed, for example \[{"min": 0, "max": 150}, {"min": 150, "max": 400}]. Splitting a busy area by price reveals listings a single view hides.

## `minPortfolioSize` (type: `integer`):

Only hosts with at least this many listings in the area scanned are saved. Set it to 2 or more to keep multi-property operators and drop one-off hosts. Bear in mind the count is a minimum — an operator whose other properties sit outside the area scanned can be dropped by this filter, so widen the area or raise the listing limit before setting it high. Smaller hosts are still counted in the run summary.

## `unregisteredOnly` (type: `boolean`):

Keep only hosts who have at least one listing with no registration or licence number visible. A blank means the number is not published where it can be seen — it is not proof that a listing is unlicensed.

## `registrationFormat` (type: `string`):

Optional pattern each registration number is checked against, for example ^\d{13}$ for a 13-digit Paris number. Every record then reports whether its number matched. Leave empty to skip the check.

## `includeLicence` (type: `boolean`):

Reads the registration or licence number from every place Airbnb publishes it, and records which of those places it was found in.

## `emitListingRows` (type: `boolean`):

Adds a row for every listing behind each host portfolio, with its own registration number, size and rating. Turn it off for a host-only dataset — that lowers both the row count and the cost of the run.

## `maxListings` (type: `integer`):

The main cost and time control: every listing here gets its own look-up to find its host. It also decides how much of an area you actually see. The default 500 is a cheap first look and is enough to finish a single district, but a whole city holds tens of thousands of listings — over a city, 500 is a thin slice, most hosts in it will show one listing, and that is a fact about the slice rather than about the market. To measure how concentrated a market is, either scan a map rectangle small enough to finish, or raise this until the coverage report stops saying the scan was cut short.

## `maxRequests` (type: `integer`):

A hard ceiling on how much work one run may do in total, counting both area scans and listing look-ups. The projected total is printed in the log before the run starts.

## `maxConcurrency` (type: `integer`):

How many listings are looked up at the same time. Higher is faster but harder on the source site; 4 is a good balance.

## `currency` (type: `string`):

Currency for the nightly rates in the output, for example USD, EUR or GBP.

## `locale` (type: `string`):

Language for names, property types and neighbourhood labels. Only English ("en") has been verified; other languages may change how some text fields read.

## `proxyCountry` (type: `string`):

Country to appear to browse from, as a two-letter code such as us, gb or fr. Leave empty unless a market shows you different results from different countries.

## Actor input object example

```json
{
  "places": [
    "Paris, France"
  ],
  "bboxes": [],
  "tileSaturationThreshold": 240,
  "maxQuadtreeDepth": 3,
  "roomTypes": [],
  "priceBands": [],
  "minPortfolioSize": 1,
  "unregisteredOnly": false,
  "registrationFormat": "",
  "includeLicence": true,
  "emitListingRows": true,
  "maxListings": 500,
  "maxRequests": 800,
  "maxConcurrency": 4,
  "currency": "USD",
  "locale": "en",
  "proxyCountry": ""
}
```

# Actor output Schema

## `dataset` (type: `string`):

No description

# 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 = {
    "places": [
        "Paris, France"
    ],
    "tileSaturationThreshold": 240,
    "maxQuadtreeDepth": 3,
    "minPortfolioSize": 1,
    "maxListings": 500,
    "maxRequests": 800,
    "maxConcurrency": 4,
    "currency": "USD",
    "locale": "en"
};

// Run the Actor and wait for it to finish
const run = await client.actor("apt_marble/airbnb-host-portfolio-mapper").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 = {
    "places": ["Paris, France"],
    "tileSaturationThreshold": 240,
    "maxQuadtreeDepth": 3,
    "minPortfolioSize": 1,
    "maxListings": 500,
    "maxRequests": 800,
    "maxConcurrency": 4,
    "currency": "USD",
    "locale": "en",
}

# Run the Actor and wait for it to finish
run = client.actor("apt_marble/airbnb-host-portfolio-mapper").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 '{
  "places": [
    "Paris, France"
  ],
  "tileSaturationThreshold": 240,
  "maxQuadtreeDepth": 3,
  "minPortfolioSize": 1,
  "maxListings": 500,
  "maxRequests": 800,
  "maxConcurrency": 4,
  "currency": "USD",
  "locale": "en"
}' |
apify call apt_marble/airbnb-host-portfolio-mapper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,apt_marble/airbnb-host-portfolio-mapper"
        }
    }
}

```

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/rpRuRtmqMhbYYVDdV/builds/32ZgDULPpq2NyQoKM/openapi.json
