# Willhaben Scraper - Ads, Prices, Locations, Sellers (`s-r/willhaben-scraper`) Actor

Scrape willhaben.at classifieds across marketplace, real estate, cars and jobs. Every ad comes back with price, the full Austrian location breakdown (city, postcode, district, state), publication date and seller. No login and no API key.

- **URL**: https://apify.com/s-r/willhaben-scraper.md
- **Developed by:** [SR](https://apify.com/s-r) (community)
- **Categories:** E-commerce, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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?

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

A willhaben.at scraper that returns classified ads with the price, the full Austrian location breakdown, the publication date and the seller, across marketplace, real estate, cars and jobs. No login, no API key, no browser.

willhaben is Austria's dominant classifieds site. A single marketplace keyword returns 95.870 ads, and nothing on the Apify Store covers it.

### What you get

- **Location broken into real columns**: town, postcode, district (Bezirk) and federal state (Bundesland), rather than one string you have to unpick. Austrian ads live and die on region, and this is what makes them filterable
- **Publication, edit and expiry timestamps** per ad, so you can tell a fresh listing from one that has been sitting for six weeks
- **Seller organisation id and a private-versus-business flag**, so a trader's whole inventory groups without a name match
- **Four verticals from one input**: marketplace, real estate, used cars and jobs
- **The site's own result count** on every run, so you know whether your 60 rows came from 300 matches or 95.000
- **The complete raw attribute map** kept on every row, so a field willhaben adds later is recoverable rather than silently lost
- **30 ads per request** straight from the page's data envelope, no per-ad fetch
- Charged per ad, plus a flat $0,002 start fee

### Why scrape willhaben

Austria is a market most scraping stacks skip. It is too small for the pan-European tools to bother localising and too distinct for a German scraper to cover, since willhaben rather than eBay Kleinanzeigen is where Austrians actually trade. For price research, recommerce sourcing or property analysis in Austria, this is the dataset.

The structure rewards a proper reader. willhaben stores every ad's fields as a flat attribute list, `PRICE/AMOUNT`, `HEADING`, `BODY_DYN`, `LOCATION`, `POSTCODE`, `STATE`, `DISTRICT`, `PUBLISHED`, each holding a list of strings. Reading the rendered card gives you a title and a price; reading the attribute list gives you the whole record including the seller id and the expiry date, neither of which appears on screen.

### Input

| Field | Type | Required | Default | What it does |
|---|---|---|---|---|
| `query` | string | yes | `fahrrad` | What to search for, German works best |
| `vertical` | select | no | `marketplace` | Marketplace, real estate, used cars or jobs |
| `url` | string | no | – | A willhaben search URL to read instead, filters included |
| `price_min` / `price_max` | integer | no | – | Price bounds in euros |
| `limit` | integer | no | `60` | Ads to return, 1 to 750 |
| `retries` | integer | no | `4` | Retry attempts per page |

### Output

```json
{
  "position": 1,
  "ad_id": "1366031979",
  "title": "Kinder fahrrad",
  "description": "Original KTM Kinderrad Wild Cat 24.21 24 Zoll ...",
  "url": "https://www.willhaben.at/iad/kaufen-und-verkaufen/d/kinder-fahrrad-1366031979/",
  "price": 250.0,
  "currency": "EUR",
  "city": "Naarn",
  "postcode": "4331",
  "district": "Perg",
  "state": "Oberösterreich",
  "country": "Österreich",
  "published_at": "2026-09-01T12:51:14Z",
  "expires_at": "2026-10-16T14:51:03Z",
  "seller_org_id": "26694604",
  "is_private_seller": false,
  "images_count": 5,
  "query": "fahrrad"
}
```

### Use cases

**Second-hand price research for Austria.** Run a product name, filter by `state`, and you have the regional price spread. Vienna and Vorarlberg are different markets for the same item, and because the state is a column rather than buried in a location string, splitting them is a group-by.

**Recommerce sourcing.** Sort by newest, set a price ceiling and filter on `is_private_seller`. Private ads at a discount are where the margin is, and `postcode` plus `district` tells you what is collectable without a day's driving.

**Property monitoring.** Switch `vertical` to real estate and the same row shape carries listings with price, location and publication date. Repeat runs on the same query show what came on and what left the market, which is the signal no portal reports.

**Dealer stock tracking.** `seller_org_id` groups a business seller's ads. Store the ids weekly and you see intake, turnover and what has been sitting, for any dealer on the platform.

### How it compares

| | this actor | `apify/e-commerce-scraping-tool` |
|---|---|---|
| Per 1.000 ads | **$1,60** | $6,00 |
| Covers willhaben | **yes** | generic, needs configuring |
| Location split into 4 columns | **yes** | no |
| Publication and expiry dates | **yes** | no |
| Private vs business seller | **yes** | no |
| Verticals | **4** | n/a |
| Needs a browser | **no** | no |

Honest about the other side: the generic actor has 575 monthly users and points at many shops. It is a breadth tool and this is a depth one. Nothing on the Store covers willhaben at all.

### Pricing

Two events. A flat `run_start` of $0,002 covers the run, and `listing` costs $0,0016 per ad returned, which is $1,60 per 1.000. Beyond the start fee you only pay for ads you actually receive.

### Limits and gotchas

- **Not every ad is in Austria.** Cross-border sellers appear, so `country` is a real column and worth filtering on before computing an Austrian market price.
- **`postcode` and `district` are empty for foreign ads**, which is correct rather than missing.
- **750 ads per run** across 25 pages of 30. The summary reports the site's own `rowsFound`, so you can see how much of the result set you actually took.
- **Prices on job and property ads mean different things** than on marketplace ads: a salary or a rent rather than an asking price. Check the vertical before comparing.
- **`description` is the listing body as shown in results**, which willhaben sometimes truncates. Opening the ad would be a second fetch per row and this actor does not make it.
- **Promoted ads are mixed into results.** `ad_type_id` distinguishes them if you need to exclude paid placements.

### FAQ

**Can I scrape willhaben without an API key?**
Yes. Every result page carries its ads as JSON for the browser to render, and that is what this actor reads. There is no credential in the input.

**Does it cover property and cars as well as the marketplace?**
Yes, four verticals share the same input and the same row shape: marketplace, real estate, used cars and jobs.

**Can I filter by region?**
Every row carries town, postcode, district and federal state as separate fields, so filter after the run. For a narrower search, apply the region filter on the site and paste the URL into `url`.

**How do I tell a dealer from a private seller?**
`is_private_seller` is a boolean, and `seller_org_id` groups a business seller's whole inventory.

**How many ads exist for my search?**
The run summary reports `rowsFound`, which is willhaben's own count for the query, independent of how many rows you asked for.

### Related Actors

- [Marktplaats Scraper](https://apify.com/s-r/marktplaats-scraper) — the Dutch equivalent
- [AutoScout24 Scraper](https://apify.com/s-r/autoscout24-scraper) — cars across nine European markets
- [Vinted Scraper](https://apify.com/s-r/vinted-scraper) — second-hand fashion across nine markets

# Actor input Schema

## `query` (type: `string`):

What to search for on willhaben.at. German terms work best.

## `vertical` (type: `string`):

Which part of willhaben to search.

## `url` (type: `string`):

A willhaben.at search URL to read instead. Paste it from your browser after applying filters. Takes precedence.

## `price_min` (type: `integer`):

Lower price bound in euros.

## `price_max` (type: `integer`):

Upper price bound in euros.

## `limit` (type: `integer`):

How many ads to return, 1 to 750. A page carries 30 and the site reports the true result count.

## `retries` (type: `integer`):

Retry attempts per page, each with a rotated TLS fingerprint.

## Actor input object example

```json
{
  "query": "fahrrad",
  "vertical": "marketplace",
  "url": "https://www.willhaben.at/iad/kaufen-und-verkaufen/marktplatz?keyword=fahrrad",
  "limit": 60,
  "retries": 4
}
```

# Actor output Schema

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

One row per classified ad.

## `summary` (type: `string`):

The site's own result count, pages fetched, and field coverage.

## `errors` (type: `string`):

Per-page failures with a code and a redacted message.

# 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 = {
    "query": "fahrrad",
    "limit": 60,
    "retries": 4
};

// Run the Actor and wait for it to finish
const run = await client.actor("s-r/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 = {
    "query": "fahrrad",
    "limit": 60,
    "retries": 4,
}

# Run the Actor and wait for it to finish
run = client.actor("s-r/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 '{
  "query": "fahrrad",
  "limit": 60,
  "retries": 4
}' |
apify call s-r/willhaben-scraper --silent --output-dataset

```

## MCP server setup

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