# willhaben Scraper & New-Listing Monitor - Austria Property (`neverempty/willhaben-listings`) Actor

Turn monitoring on and every later run returns only the listings it has not returned before, so you are never charged for the same property twice. Every willhaben.at listing as clean JSON: price with its meaning, price per m2, rooms, area, postcode, coordinates. No private sellers named. Unofficial.

- **URL**: https://apify.com/neverempty/willhaben-listings.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** Real estate, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.73 / 1,000 property returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#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

## willhaben Scraper - Austria Real Estate Data, Price & Size

Pull property listings from **willhaben.at** — Austria's largest property portal — as clean JSON: the price as a number and what it means, price per m², rooms, living and plot area, floor, outdoor spaces, address with postcode and coordinates, and the agency, for flats, houses and land, for sale or to rent, anywhere in Austria.

*Unofficial. This Actor is not affiliated with, endorsed by, or sponsored by willhaben. All product names are trademarks of their respective owners.*

No API key. Reads the same public pages a visitor sees.

***

### Run it tomorrow and you only get what is new

Schedule this Actor with `monitoringMode: true` and each run returns **only the listings it has not returned before**. Not "here is everything again, please de-duplicate it yourself" — the Actor remembers what it already gave you, so a daily feed of a 4,000-listing search costs you the handful of new homes, not 4,000 rows every morning. **You are never charged for the same property twice.**

Listings your filters removed are remembered too, so widening a filter later does not resurrect them as false "new" listings. Listings that did not fit under your limit are **not** remembered, so they are still waiting on the next run instead of being silently lost.

***

### Why this one is different

#### The price means what it says

willhaben stores every price in the same field, whether it is a sale total or a monthly rent. This Actor labels it: `priceType: "sale"` on sales and `priceType: "monthly-rent"` on rentals (median rent in the 30 Vienna flats to rent measured: **€1,249** a month; median price of the 80 unique Vienna flats for sale: **€476,000**). Land priced *Preis auf Anfrage* (on request) comes back as `price: null` with the display text in `priceDisplay` and `isPriceOnRequest: true` — never as a zero. willhaben's own price per m² is passed through in `pricePerSqm`.

#### Size means the right thing for the category

willhaben's main size field is the living area for flats and houses, but the plot for land. This Actor returns `sizeSqm` (willhaben's figure) plus `livingAreaSqm`, `usableAreaSqm` and `plotAreaSqm`, each filled only where it is the right meaning: all 30 land listings measured have `plotAreaSqm` and none has `livingAreaSqm`.

#### The publish time is the real one

willhaben ships two publish fields: a Unix timestamp and a text version that carries local Austrian time with a `Z` suffix — two hours in the future on every one of the 210 listings measured. `publishedAt` is built from the timestamp, so it never post-dates `scrapedAt`.

#### "0 rooms" is not sold as zero rooms

willhaben writes `0` in the room count on listings that simply did not fill it in (14 of the 210 measured, including a 188 m² detached house). Those come back as `rooms: null`, not `0`.

#### Private sellers are never named — because willhaben does not name them either

Private listings (26 of the 210 measured) carry no name on willhaben, only a *Privat* label. This Actor returns `advertiserType: "private"` with `agencyName: null` for them and the agency name only for agency listings; Austrian phone numbers and e-mail addresses written into the text are removed. Property data, not a contact list.

#### An unknown place is a 404, and it is reported as one

willhaben answers a state or district it does not know with an honest 404. This Actor returns a `no-such-search` row that lists how willhaben spells its states and districts — distinct from `no-results`, which is willhaben's own "zero listings" answer. Both are free.

#### One request, 30 complete listings

willhaben embeds the full data of every listing in the search page. This Actor reads all 30 from that single page and **never opens individual listing pages**. willhaben's second page starts at the 21st listing, so 9 of its 30 cards were already on page one; repeats are dropped before delivery, so **you are never charged twice for the same listing**, and the page budget is sized for 21 new listings per page rather than 30.

#### Filters run here, because willhaben's robots.txt says so

willhaben's `robots.txt` disallows price and keyword query parameters (`PRICE_FROM`, `PRICE_TO`, `keyword`, `TYPE` …). This Actor therefore requests only the plain path plus `?page=N`, and every filter — price, price per m², rooms, size, postcode, property type, advertiser, outdoor space, keywords, agency, publish date — runs on the rows already fetched. Nothing extra is requested and nothing extra is charged. A listing is never dropped for a value willhaben did not publish.

***

### Input

Either paste a search URL, or build one from the fields below.

| Field | Type | Default | Description |
|---|---|---|---|
| `searchUrl` | string | — | e.g. `https://www.willhaben.at/iad/immobilien/eigentumswohnung/wien`. Query parameters are dropped |
| `category` | string | `flat-buy` | `flat-buy`, `flat-rent`, `house-buy`, `house-rent`, `land` |
| `state` | string | `wien` | `wien`, `niederoesterreich`, `oberoesterreich`, `salzburg`, `tirol`, `vorarlberg`, `kaernten`, `steiermark`, `burgenland`. Empty = all of Austria |
| `district` | string | — | As willhaben writes it: `wien-1020-leopoldstadt`, `salzburg-stadt`. Needs the state |
| `maxListings` | integer | 20 | 1-1000. One listing = one row = one charged event |
| `maxPages` | integer | 0 | Hard cap on requests; 0 = decide from `maxListings` |
| `includeDescription` | boolean | `true` | The card text. The keyword filter searches it either way |
| `useProxy` | boolean | `false` | Off by default — willhaben answers Apify's network directly. Switches on by itself if blocked |
| `keywords` / `keywordMatch` / `excludeKeywords` | array / string / array | — | Match on title, description, type, address, district, outdoor spaces and agency |
| `agencyNames` | array | — | Keep only these agencies (partial match) |
| `minPrice` / `maxPrice` | integer | 0 | EUR. Sale total or monthly rent. **A listing priced on request is kept** |
| `maxPricePerSqm` | integer | 0 | EUR per m², willhaben's figure |
| `requirePrice` | boolean | `false` | Drop listings priced on request |
| `minRooms` / `maxRooms` | integer | -1 | -1 = off. **An unfilled room count is kept** |
| `minSizeSqm` / `maxSizeSqm` | integer | 0 | willhaben's main size. 0 = off |
| `postcodes` | array | — | 4-digit Austrian postcodes |
| `propertyTypes` | array | — | willhaben's German words, partial match: `Wohnung`, `Dachgeschoßwohnung`, `Einfamilienhaus`, `Reihenhaus`, `Villa`, `Baugrundstück` … |
| `advertiserType` | string | `any` | `agency` or `private` |
| `outdoorSpaces` | array | — | `Balkon`, `Terrasse`, `Garten`, `Loggia`. **A listing with none published is kept** |
| `excludeProjects` | boolean | `false` | Drop listings that belong to a development project or advertise several units ("from" prices are flagged `isPriceFrom`) |
| `publishedWithinDays` | integer | 0 | By willhaben's publish timestamp. 0 = off |
| `monitoringMode` | boolean | `false` | Return only listings not returned on a previous run |
| `resetMonitoringState` | boolean | `false` | One-shot: forget what was already returned |

```json
{ "category": "flat-buy", "state": "wien", "district": "wien-1020-leopoldstadt", "maxListings": 100,
  "minRooms": 3, "maxPrice": 500000, "maxPricePerSqm": 7000, "outdoorSpaces": ["Balkon", "Terrasse"], "advertiserType": "agency" }
```

#### Monitoring mode turns this into a daily feed

Schedule it and set `monitoringMode: true`: each run returns **only the listings it has not returned before**. Listings removed by your filters are remembered too, so changing a filter later does not resurrect them as false "new" listings — and listings that simply **did not fit under `maxListings`, or were cut off by the run's charge limit, are not remembered**, so they are still waiting for you on the next run rather than being silently lost. willhaben lists newest first, so a small daily run catches new stock.

Measured on 2026-09-05: **20,089** flats for sale in Vienna; **4,645** flats to rent in Vienna; **418** plots in Tyrol.

***

### Output

One row per listing:

```json
{
  "source": "willhaben.at",
  "status": "ok",
  "listingId": "1459808476",
  "title": "Exklusives Airbnb-Apartment mit Büro im 1. Bezirk - Zentrum Wien",
  "description": "Zum Verkauf gelangt ein attraktives Apartment mit zusätzlichem Büro …",
  "url": "https://www.willhaben.at/iad/immobilien/d/eigentumswohnung/wien/wien-1020-leopoldstadt/…-1459808476/",
  "searchedLocation": "Eigentumswohnungen in Wien",
  "category": "flat-buy",
  "transactionType": "sale",
  "propertyType": "Wohnung",
  "propertyTypeId": "3",
  "price": 169000,
  "priceCurrency": "EUR",
  "priceType": "sale",
  "priceDisplay": "€ 169.000",
  "isPriceOnRequest": false,
  "pricePerSqm": 5633.3335,
  "rooms": null,
  "sizeSqm": 30,
  "livingAreaSqm": 30,
  "usableAreaSqm": null,
  "plotAreaSqm": null,
  "floor": null,
  "outdoorSpaces": [],
  "outdoorAreaSqm": null,
  "address": "Czerninplatz",
  "postcode": "1020",
  "location": "Wien, 02. Bezirk, Leopoldstadt",
  "district": "Wien",
  "state": "Wien",
  "country": "Österreich",
  "latitude": 48.21454,
  "longitude": 16.38717,
  "advertiserType": "private",
  "agencyName": null,
  "isPriceFrom": false,
  "isProject": false,
  "projectUnits": null,
  "isPromoted": false,
  "isBumped": true,
  "imagesCount": 9,
  "mainImageUrl": "https://cache.willhaben.at/mmo/6/145/980/8476_-265142440.jpg",
  "publishedAt": "2026-09-05T07:56:07.200Z",
  "scrapedAt": "2026-09-05T08:19:55.370Z"
}
```

Measured across 210 live listings (200 unique) on 2026-09-05 — flats for sale, flats to rent, houses for sale, houses to rent and land: id, title, description, url, location, district, state, coordinates and publish date on **210 of 210**; postcode on **209**; size on **208**; a numeric price on **204**; price per m² on **202**; rooms on **166**; outdoor spaces on **145**; floor on **89**; a street address on **87**.

#### Rows that are never charged

| `status` | when |
|---|---|
| `no-results` | willhaben reports zero listings for this search. That is its answer, not a failure |
| `no-such-search` | willhaben has no page at that address (HTTP 404) - a state or district spelled differently from willhaben's addresses. **Not** a claim that there is nothing there |
| `no-filter-match` | Listings were read, but your filters removed all of them. The row says how many were read |
| `no-new-listings` | Monitoring mode: nothing new since the previous run |
| `unreadable` | The page could not be read, its embedded data was missing, or it claimed a total above zero while yielding nothing |

***

### Pricing

Pay per listing returned. Rows that report an empty search, an unknown address, a filter that matched nothing, or a page that could not be read are **not** charged. A listing that appears twice across pages is dropped before delivery, so it is never charged twice.

***

### Notes

- `robots.txt` was read in full on 2026-09-05. The `User-agent: *` section disallows `/iad/admin`, `/iad/myprofile`, `/iad/searchagent`, `/mob/`, `/pal/` and the query parameters `counterId`, `PRICE_FROM`, `PRICE_TO`, `SEGMENT`, `TYPE`, `keyword`, `periode`, `sid`. This Actor requests only `/iad/immobilien/<category>/<state>[/<district>]` with `?page=N`.
- Only public pages are read. No login, no API key.

# Actor input Schema

## `searchUrl` (type: `string`):

Paste a willhaben property listing page, for example https://www.willhaben.at/iad/immobilien/eigentumswohnung/wien or https://www.willhaben.at/iad/immobilien/mietwohnungen/wien/wien-1020-leopoldstadt, and the three fields below are ignored. Query parameters are dropped (willhaben's robots.txt forbids price and keyword parameters), so every filter runs here on the rows already fetched.

## `category` (type: `string`):

The five willhaben property categories checked on live pages.

## `state` (type: `string`):

wien, niederoesterreich, oberoesterreich, salzburg, tirol, vorarlberg, kaernten, steiermark, burgenland - as willhaben writes them (umlauts become ae/oe/ue). Leave empty for the whole of Austria. A state willhaben does not know comes back as a no-such-search row (willhaben answers 404), not as an empty result.

## `district` (type: `string`):

As willhaben writes it in its addresses: wien-1020-leopoldstadt, salzburg-stadt, graz. Needs the state.

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

How many properties to return. You are charged for the rows you actually receive.

## `maxPages` (type: `integer`):

A hard stop on how many pages are requested, whatever the filters do. 0 lets the run work it out.

## `includeDescription` (type: `boolean`):

The listing text willhaben shows on the card. Turn it off for a smaller dataset - the keyword filter still searches it either way.

## `useProxy` (type: `boolean`):

Off by default because willhaben answers Apify's own network directly. If willhaben ever starts blocking, the run switches to a proxy on its own.

## `keywords` (type: `array`):

Keep only listings whose title, description, type, address, district, outdoor spaces or agency contain these words. No extra requests are made.

## `keywordMatch` (type: `string`):

any: at least one keyword. all: every keyword.

## `excludeKeywords` (type: `array`):

Drop listings containing any of these words.

## `agencyNames` (type: `array`):

Keep only listings marketed by these agencies. Partial names work.

## `minPrice` (type: `integer`):

A sale price is a total; a rent is per month, the way willhaben publishes it. 0 means no minimum.

## `maxPrice` (type: `integer`):

0 means no maximum. A listing whose price is 'Preis auf Anfrage' (on request) is kept.

## `maxPricePerSqm` (type: `integer`):

willhaben's own figure. 0 means no maximum.

## `requirePrice` (type: `boolean`):

Off by default: a listing priced 'on request' is not treated as a cheap one, it is simply kept.

## `minRooms` (type: `integer`):

-1 turns the filter off. A listing without a room count is kept.

## `maxRooms` (type: `integer`):

-1 turns the filter off.

## `minSizeSqm` (type: `integer`):

Living area for flats and houses, plot area for land - willhaben's main size figure. 0 means no minimum.

## `maxSizeSqm` (type: `integer`):

0 means no maximum.

## `postcodes` (type: `array`):

Keep only these 4-digit Austrian postcodes (1020, 5020 …).

## `propertyTypes` (type: `array`):

willhaben's German words, partial match: Wohnung, Dachgeschoßwohnung, Erdgeschoßwohnung, Maisonette, Penthouse, Einfamilienhaus, Reihenhaus, Villa, Baugrundstück …

## `advertiserType` (type: `string`):

Agency listings, or listings from private owners. Private owners are never named by willhaben.

## `outdoorSpaces` (type: `array`):

Keep only listings with one of these, as willhaben names them: Balkon, Terrasse, Garten, Loggia. A listing that publishes no outdoor space is kept.

## `excludeProjects` (type: `boolean`):

Drop listings that belong to a development project.

## `publishedWithinDays` (type: `integer`):

By willhaben's publish timestamp. 0 means no date filter. A listing whose date cannot be read is kept.

## `monitoringMode` (type: `boolean`):

Turn this on and schedule the Actor: each run returns only the listings it has never returned before, so you are charged for new stock rather than for the same pages again. Listings that did not fit under the limit are still waiting for you on the next run.

## `resetMonitoringState` (type: `boolean`):

Clears the memory for this search, so the next monitoring run starts from scratch. Works with or without monitoring mode on.

## Actor input object example

```json
{
  "category": "flat-buy",
  "state": "wien",
  "district": "",
  "maxListings": 20,
  "maxPages": 0,
  "includeDescription": true,
  "useProxy": false,
  "keywords": [],
  "keywordMatch": "any",
  "excludeKeywords": [],
  "agencyNames": [],
  "minPrice": 0,
  "maxPrice": 0,
  "maxPricePerSqm": 0,
  "requirePrice": false,
  "minRooms": -1,
  "maxRooms": -1,
  "minSizeSqm": 0,
  "maxSizeSqm": 0,
  "postcodes": [],
  "propertyTypes": [],
  "advertiserType": "any",
  "outdoorSpaces": [],
  "excludeProjects": false,
  "publishedWithinDays": 0,
  "monitoringMode": false,
  "resetMonitoringState": false
}
```

# Actor output Schema

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

One row per listing: title and description, the price as a number with what it means (a sale total or a monthly rent) and willhaben's own display text, 'on request' kept as null, price per m2, rooms, willhaben's main size plus living, usable and plot area where published, floor, outdoor spaces, address, postcode, district and state with coordinates, whether the advertiser is an agency or a private owner (never named), project and promotion flags, the main image and the publish date. Every row also names the search willhaben actually ran. Searches with no results, addresses willhaben does not have, filters that matched nothing, and pages that could not be read come back as their own rows and are not charged.

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/willhaben-listings").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("neverempty/willhaben-listings").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 '{}' |
apify call neverempty/willhaben-listings --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/willhaben-listings"
        }
    }
}

```

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/o3Mbaygyz173OlkC8/builds/x2f3vGqs15U3FIwqn/openapi.json
