# Willhaben.at Real Estate Scraper (`artsiom_k/willhaben-scraper`) Actor

Austria real estate scraper for Willhaben.at — apartments, houses, land & commercial, rent or sale — with province filters and built-in delta mode.

- **URL**: https://apify.com/artsiom\_k/willhaben-scraper.md
- **Developed by:** [Artsiom Kunitsyn](https://apify.com/artsiom_k) (community)
- **Categories:** Real estate
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.50 / 1,000 results

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

## willhaben-scraper

Scrapes real estate listings from [Willhaben.at](https://www.willhaben.at) (apartments, houses,
land, commercial — rent or sale) via Willhaben's own internal JSON search API, not HTML scraping.

### Contents

- [Key features](#key-features)
- [Output](#output)
- [Input](#input)
- [Input examples](#input-examples)
- [Incremental (delta) mode](#incremental-delta-mode)
- [How to scrape Willhaben.at](#how-to-scrape-willhabenat)
- [How much does it cost](#how-much-does-it-cost)
- [You might also like](#you-might-also-like)
- [FAQ](#faq)

### 🔑 Key features

- **Real-estate specialist, not a jack-of-all-trades.** Built for Willhaben's real-estate
  categories specifically, rather than spreading thin across cars, jobs, and marketplace listings
  too.
- **No URL wrangling.** Pick a category and an Austrian province from a dropdown — no need to
  construct or paste a Willhaben search URL yourself.
- **Delta mode built in.** Every run classifies each listing as `new`, `changed`, `unchanged`, or
  `delisted` against a persisted baseline. Run it on a schedule and you're not paying for a full
  re-scrape every time — just what actually changed. It's also what makes price-history tracking
  possible at all.
- **Full category coverage, including the categories many scrapers skip** — land/plots and
  commercial, not just apartments.
- **Privacy-aware by construction.** `agency_name` is populated for professional listings only
  (company name) and left `null` for private sellers — no private-seller personal data leaves the
  dataset.

### 📋 Output

One dataset item per listing, with a normalized schema (see
[`.actor/dataset_schema.json`](.actor/dataset_schema.json) for the full field list) plus a
`change_type` field: `new`, `changed`, `unchanged`, or `delisted` (see
[Incremental mode](#incremental-delta-mode) below).

**Example job record:**

```json
{
  "source": "willhaben",
  "external_id": "1234567890",
  "url": "https://www.willhaben.at/iad/immobilien/d/mietwohnungen/wien/wien-1220-donaustadt/...",
  "country": "AT",
  "state": "Wien",
  "district": "Donaustadt",
  "city": "Wien",
  "postal_code": "1220",
  "latitude": 48.2333,
  "longitude": 16.4667,
  "price_eur": 1450.0,
  "price_per_sqm_eur": 19.33,
  "living_area_sqm": 75.0,
  "estate_size_sqm": 75.0,
  "rooms": 3,
  "listing_type": "rent",
  "property_class": "apartment",
  "property_type": "Wohnung",
  "has_balcony": true,
  "has_garden": false,
  "has_parking": false,
  "agency_name": "Muster Immobilien GmbH",
  "published_at": "2026-08-01T09:12:00.000Z",
  "change_type": "new"
}
```

Every record also carries `condition`, `year_built`, and `energy_class` — omitted from the example
above because they're currently always `null` (see known gap below).

Results can be downloaded as JSON, CSV, or Excel from the Console's Output tab, or pulled via the
Apify API/dataset endpoint.

**Known gap:** `condition`, `year_built`, and `energy_class` are always `null` — Willhaben's search
API doesn't carry them, and the one endpoint that does blocks both plain and TLS-impersonated
requests.

### ⚙️ Input

See [`.actor/input_schema.json`](.actor/input_schema.json) for the full JSON schema. Key
parameters:

| Parameter | Type | Default | Description |
|---|---|---|---|
| `category` | String (required) | — | One of 7 categories: `apartment_rent`, `apartment_sale`, `house_rent`, `house_sale`, `land`, `commercial_rent`, `commercial_sale`. |
| `region` | String | `all` | One of Austria's 9 provinces (e.g. `wien`, `steiermark`), or `all` for the whole country. Changing this between runs starts a fresh incremental-mode baseline for the category. |
| `mode` | String | `auto` | `auto` (recommended): full scan on the first run, incremental after. `full`: always push every listing and refresh the baseline. `incremental`: always push only new/changed listings. See [Incremental mode](#incremental-delta-mode). |
| `maxItems` | Integer | `50` | Stop after pushing this many dataset items. Defaults to a fast, cheap preview (also what keeps an unconfigured run within Apify's automated 5-minute QA check). Raise it or clear it (set to `null`) for a full run — note a capped run never updates the incremental baseline (see below), so any real tracking run needs this cleared. |
| `maxPages` | Integer | *(none)* | Safety cap on search pages fetched. A capped run can't detect delistings and won't update the incremental baseline. |
| `rows` | Integer | `200` | Listings per search page (1–200). |
| `delaySeconds` | Number | `3.5` | Politeness delay between consecutive search-page requests. |
| `proxyConfiguration` | Object | `{"useApifyProxy": false}` | Apify Proxy config. Off by default — confirmed by testing that Willhaben returns HTTP 403 to Apify's shared datacenter proxy pool, while a direct connection works fine. Only reach for proxy (residential, not datacenter) if you start seeing blocks. |
| `impersonate` | String | *(none)* | Optional curl\_cffi TLS-impersonation target (e.g. `chrome124`), used only if Willhaben starts blocking plain requests. |

### 🧪 Input examples

**Quick preview** — Wien apartments for rent, default 50-item cap:

```json
{
  "category": "apartment_rent",
  "region": "wien"
}
```

**Full-country scan** — every land/plot listing in Austria, capped for a quick preview:

```json
{
  "category": "land",
  "region": "all",
  "maxItems": 100
}
```

**Scheduled tracking run** — full, uncapped run (`maxItems` cleared — required for the baseline to
save and delistings to be detected, see [Incremental mode](#incremental-delta-mode)):

```json
{
  "category": "apartment_rent",
  "region": "wien",
  "mode": "incremental",
  "maxItems": null
}
```

### 🔄 Incremental (delta) mode

Every run classifies each listing as `new`, `changed` (price moved), `unchanged`, or `delisted`,
using a state baseline persisted in a named Apify Key-Value Store scoped to `category` + `region`.

- `mode: auto` (default) — first run for a scope pushes everything (`full`); later runs push only
  `new`/`changed`/`delisted` (`incremental`).
- Recommended setup: schedule a frequent Task on `auto`, plus a periodic (e.g. weekly) Task
  explicitly on `mode: full` to force a full resync — a page-capped or otherwise partial run never
  updates the baseline or reports delistings, so a periodic full run keeps the baseline honest.

Full design: [`docs/incremental-mode.md`](../../docs/incremental-mode.md).

### 🚀 How to scrape Willhaben.at

1. Open the Willhaben.at Real Estate Scraper in Apify Console and go to the **Input** tab.
2. Pick a **category** (e.g. "Apartments — rent") and a **region** (e.g. "Wien"), or leave region
   at "All of Austria" for full country coverage.
3. `maxItems` defaults to 50 (a quick preview) — clear it (set to `null`) along with `maxPages` for
   a full, uncapped run.
4. Click **Start**.
5. When the run finishes, browse results in the **Output** tab, or download as JSON/CSV/Excel, or
   fetch them via the API.
6. To track a market over time instead of scraping once: create a **Schedule** with `mode: auto` —
   the first run does a full scan, every run after only bills the listings that actually changed.

### 💰 How much does it cost

Pricing: **$3.50 per 1,000 results** (pay-per-result), plus a fixed, negligible per-run start fee.
No cost for pages that return zero results.

**Example:** a full scan of Wien apartment-rent listings (~4,700 listings, confirmed by a real
test run) costs roughly **$16.40** for the initial full scan. Because of built-in delta mode, a
scheduled re-run afterwards only bills for listings that are actually `new`, `changed`, or
`delisted` — not the full ~4,700 again — so ongoing monitoring costs a fraction of the initial
scan once the baseline is established.

### 🔗 You might also like

- **[ImmoScout24 (AT) Real Estate Scraper](https://apify.com/artsiom_k/immoscout24-at-scraper)** —
  a second, independent Austrian real-estate source with the identical output schema by design, so
  results from both merge directly for cross-checking coverage or de-duplicating listings posted on
  both sites.
- **[Willhaben.at Vehicles Scraper](https://apify.com/artsiom_k/willhaben-vehicles-scraper)** — the
  sibling actor for Willhaben's other big vertical: used cars, motorcycles, commercial vehicles, and
  caravans/motorhomes. Same delta-mode design, same province filters — useful if you're tracking a
  seller or household's full footprint on Willhaben, not just real estate.

### ❓ FAQ

**Is it legal to scrape Willhaben.at?** It's legal to collect publicly available listing data such
as prices, descriptions, and locations. Results may contain limited personal data (agency names for
professional listings only — see [Key features](#key-features)); scrape it only with a legitimate
purpose under GDPR.

**How do I get only new/changed listings?** Use `mode: auto` (or `incremental`) on a schedule — see
[Incremental mode](#incremental-delta-mode).

**Why is a field always `null` for some listings?** `condition`, `year_built`, and `energy_class`
are always `null` — see [Output](#output) for why.

### Search keywords

willhaben scraper, willhaben.at scraper, austria real estate scraper, austrian property scraper,
immobilien scraper, wien real estate data, apartment scraper austria, house scraper austria,
willhaben api, willhaben immobilien api, real estate delta scraper, real estate price tracking,
austria property data feed

# Actor input Schema

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

Which Willhaben real-estate search category to scrape.

## `region` (type: `string`):

Restrict the search to one Austrian province, or scrape the whole country. Note: changing this between runs starts a fresh incremental-mode baseline for this category.

## `rows` (type: `integer`):

How many listings Willhaben returns per search page.

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

Safety cap on the number of search pages fetched. Leave empty to paginate to completion. Note: a capped run cannot detect delisted listings and does not update the incremental-mode baseline (see docs/incremental-mode.md).

## `maxItems` (type: `integer`):

Stop after pushing this many dataset items. Defaults to 50 — a fast, cheap preview, and what keeps an unconfigured run within Apify's automated 5-minute QA check. Raise it or clear it (set to null) for a full run.

## `delaySeconds` (type: `number`):

Politeness delay between consecutive search-page requests.

## `impersonate` (type: `string`):

Optional curl\_cffi browser TLS-impersonation target (e.g. "chrome124"), used if Willhaben starts blocking plain requests. Leave empty for plain requests.

## `mode` (type: `string`):

"auto" (recommended): full scan on the first run for a given category/area, incremental (new/changed only) afterwards. "full": always push every listing and refresh the baseline — schedule this periodically to catch delistings. "incremental": always push only new/changed listings. See docs/incremental-mode.md.

## `proxyConfiguration` (type: `object`):

Apify Proxy configuration. Confirmed by testing: Willhaben returns HTTP 403 to Apify's default (shared datacenter) proxy pool, while a direct connection from the Actor's own container works fine. Leave this off unless you start seeing blocks from your own IP reputation; if so, try residential rather than datacenter groups.

## Actor input object example

```json
{
  "category": "apartment_rent",
  "region": "all",
  "rows": 200,
  "maxItems": 50,
  "delaySeconds": 3.5,
  "mode": "auto",
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

// Run the Actor and wait for it to finish
const run = await client.actor("artsiom_k/willhaben-scraper").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("artsiom_k/willhaben-scraper").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 artsiom_k/willhaben-scraper --silent --output-dataset

```

## MCP server setup

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

```

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/alqsaAv9cvMuI6cys/builds/yiLMl37yzuj3qayDP/openapi.json
