# Willhaben Listings Scraper & New-Ad Alerts (Austria) (`superslowsloth/willhaben-listings`) Actor

Search willhaben.at, Austria's biggest classifieds site, by keyword or any search URL: one flat row per ad with title, price, location, publish time, seller type, photos and link. Real estate and used cars too. Newest first, with a 'published after' cutoff for new-ad alerts.

- **URL**: https://apify.com/superslowsloth/willhaben-listings.md
- **Developed by:** [Superslow Sloth](https://apify.com/superslowsloth) (community)
- **Categories:** E-commerce, Real estate
- **Stats:** 2 total users, 1 monthly users, 69.2% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.62 / 1,000 listing scrapeds

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Willhaben Listings Scraper & New-Ad Alerts (Austria)

Search [willhaben.at](https://www.willhaben.at), Austria's largest classifieds
site, by **keyword** or by **any willhaben search URL** and get one flat row per
ad: title, link, **price**, location with postcode and coordinates, **publish
time**, private or business seller, photos, and for real-estate and used-car
searches the area, rooms, property type, year and mileage. Run it on a schedule,
newest first with `publishedAfter` set to your last run, and you have a
**new-listing alert** that pays only for the new ads.

No account, no API key, no login, no browser. One request returns up to 200 ads.

### Input

| Field | Notes |
|---|---|
| `keywords` | Marketplace (kaufen und verkaufen) search terms, one per line. |
| `startUrls` | Any willhaben search URL from your browser's address bar: a marketplace search with filters, a real-estate search (`/iad/immobilien/...`, rentals or sales) or a used-car search (`/iad/gebrauchtwagen/...`). Filters in the URL are kept; its `page`, `rows` and `sort` parameters are replaced. At least one of `keywords` and `startUrls` is required. |
| `maxItems` | Ads to read **per keyword and per URL** (default 100, up to 100,000). |
| `sort` | `newest` (default; willhaben's own newest-first), `relevance`, `price-asc`, `price-desc`, or `as-in-url` (send no sort, so a start URL keeps the one it was copied with). |
| `publishedAfter` | Optional ISO 8601 date or time (`2026-10-13`, `2026-10-13T08:00:00Z`; UTC when no zone). Only ads published at or after it are returned, and reading stops once a page is older than it. Needs `newest`. |
| `proxyConfiguration` | Apify proxy by default. |

An ad that two searches both find is returned once, under the first.

#### Using it for new-listing alerts

Set `sort` to `newest` and `publishedAfter` to the time of your last run. Ads
come back newest first and the actor stops at the first page whose last ad is
older than the cutoff, so a run with nothing new costs the start fee and one
request. Diff on `ad_id` if you would rather not track a timestamp.

`publishedAfter` compares against `published_at`, willhaben's publication time.
Willhaben also lets sellers "bump" an ad back to the top of the list; a bumped
ad keeps its original `published_at` and is not treated as new.

### Output

Prices are numbers in euro; times are ISO 8601 in UTC (`2026-10-12T22:25:19Z`).

| Field | Notes |
|---|---|
| `query` | The keyword, or the start URL, that first found this ad. |
| `ad_id`, `url` | Willhaben's ad id (stable for the life of the ad: diff on it) and the `https://www.willhaben.at/iad/...` link. |
| `title`, `description_snippet` | The headline, and the opening of the ad text **as willhaben cuts it for the search page** (about 300 characters, ending in `...` when cut). The full text is on the ad page and is not fetched. |
| `vertical_id` | 5 marketplace, 2 real estate, 3 used cars. |
| `price`, `price_text`, `old_price` | The ask in euro (the monthly rent for a rental), the price as shown (`€ 1.390`, `zu verschenken`), and the earlier price when the seller lowered it. |
| `published_at`, `modified_at`, `expires_at` | When the ad went up, last changed and expires. |
| `location`, `postcode`, `state`, `district`, `country`, `address`, `latitude`, `longitude` | As willhaben shows them. `address` only when the seller published one. |
| `is_private`, `seller_name` | True for a private seller; the business name for a commercial one. |
| `image_url`, `image_urls` | The cover photo and every photo willhaben lists for the ad, as full URLs. The search record names every photo but links only the first three; the rest are built with willhaben's own URL pattern (checked: a fourth photo and a `.png` one both answered 200). |
| `property_type`, `living_area_m2`, `rooms`, `floor`, `price_per_m2` | Real-estate ads only. |
| `year_model`, `mileage_km` | Used-car ads only. |

#### Honest nulls

A field willhaben did not send is `null`, never `0` or `""`. The columns for
another kind of ad are null (a bicycle has no `rooms`). Two details worth
knowing:

- **`price` 0 is willhaben's own value for an ad given away.** It always came
  with `price_text` `zu verschenken` in the sample. An ad with no price at all
  (one rental in 200 had none) has `price` null.
- **`rooms` and `living_area_m2` are null when willhaben sends 0.** It writes 0
  for "not stated" (a 152 m2 penthouse had `NUMBER_OF_ROOMS` 0), so a 0 would
  read as a measurement. Ads where the seller did state a count keep it.

### Pricing

Pay per event:

- **$0.002** per run (`actor-start`), charged only after your input is
  validated, so a run that fails on bad input costs nothing.
- **$0.00062** per ad row written (`listing-scraped`), i.e. $0.62 per 1,000
  ads. A row that is not written is not charged.

### How it fetches

One plain HTTP request per page of up to 200 ads, no browser:

`https://www.willhaben.at/iad/kaufen-und-verkaufen/marktplatz?keyword=<kw>&sort=1&rows=200&page=<n>`
(or the path of your start URL). The page is a Next.js page; the ads are read
from the JSON in its `__NEXT_DATA__` script tag
(`searchResult.advertSummaryList`), where each ad is a list of named
attributes (`PRICE`, `PUBLISHED_String`, `LOCATION`, ...).

Willhaben sits behind Cloudflare. Measured from an Apify datacenter exit on
2026-10-13, about 40-45% of requests were refused with a 403, interleaved per
exit rather than adapting to the client. The actor presents a Chrome TLS
fingerprint, treats a 403, 429, 202, 5xx or a 200 without the data block as a
refusal, and retries it from a fresh proxy address, which absorbs it. From a
plain home or office address the same requests answered 200 every time in the
test; expect your own mileage.

Things worth knowing, all measured:

- **The page cap is 999 pages, not a number of ads.** Page 1000 is a 404. At
  willhaben's default of 30 ads a page that would be 29,970 ads; the actor asks
  for up to 200 ads a page (`rows` is clamped at 200), so a search of 95,933
  ads is readable to the end. A page past the real end is an empty list.
- **A wrong sort fails silently on willhaben's side.** An unknown sort id is a
  200 in relevance order with no sort marked as applied. The actor checks the
  page's own record of the applied sort for `newest` and fails the search, at
  no charge, rather than return ads in the wrong order.
- **A wrong path is not an error.** A category slug that willhaben does not
  recognise returns the *whole marketplace* (13.6 million ads) instead of a 404.
  Copy the URL from the address bar after choosing the category in the
  browser, and check `totals found` in the log against the count the site
  shows.
- **`description_snippet` is cut by willhaben**, not by this actor.
- **Counts move.** Ads are posted between requests, so newest-first paging can
  show an ad twice across a page boundary; it is returned once.
- **Not offered.** Filters beyond what is in the URL, and the ad detail page
  (full text, seller contact details, phone numbers), are not fetched.

# Actor input Schema

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

What you would type into willhaben's marketplace search box, one keyword per line (for example fahrrad, iphone 14 or kühlschrank). Each keyword is searched on the marketplace (kaufen und verkaufen) and read separately. An ad that two searches both find is returned once, under the first.

## `startUrls` (type: `array`):

Any willhaben search URL copied from your browser's address bar, one per line: a marketplace search with filters, a real-estate search (for example https://www.willhaben.at/iad/immobilien/mietwohnungen/mietwohnung-angebote) or a used-car search. The filters in the URL are kept. Its page, rows and sort parameters are replaced by this actor's own. Only URLs on willhaben.at under /iad/ are accepted. Use this, or Keywords, or both.

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

Read at most this many ads for each keyword and each URL. Willhaben serves up to 200 ads per request and 999 requests per search, so a very large search is read in part; narrow it with a more specific keyword or filters.

## `sort` (type: `string`):

The order willhaben lists the results in. 'Newest first' puts the most recently published ads first, which is the order to use for new-listing alerts, and the actor checks that willhaben really applied it. 'Relevance' is willhaben's relevance order. 'Price' sorts by the ask (by rent in a rental search). 'As in the URL' sends no sort, so a start URL keeps the sort it was copied with.

## `publishedAfter` (type: `string`):

Optional. An ISO 8601 date or time, such as 2026-10-13 or 2026-10-13T08:00:00Z (UTC when no zone is given). Only ads published at or after it are returned, and the actor stops reading once a page is older than it, so a scheduled run that passes its last run's time pays only for the new ads. Needs the 'Newest first' sort.

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

Apify proxy (automatic datacenter exits) is the default. Willhaben's Cloudflare layer refuses a share of exit addresses with a 403; the actor retries a refused request from a fresh address, which is what the proxy is for.

## Actor input object example

```json
{
  "keywords": [
    "fahrrad"
  ],
  "startUrls": [],
  "maxItems": 100,
  "sort": "newest",
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `items` (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 = {
    "keywords": [
        "fahrrad"
    ],
    "startUrls": [],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("superslowsloth/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 = {
    "keywords": ["fahrrad"],
    "startUrls": [],
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("superslowsloth/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 '{
  "keywords": [
    "fahrrad"
  ],
  "startUrls": [],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call superslowsloth/willhaben-listings --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,superslowsloth/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/bRUJAZsXq15V5yPGf/builds/o2D62nY5MZFgOY482/openapi.json
