# Immowelt Scraper — Germany Real Estate Listings | $1.19/1K (`bovi/immowelt-listings-scraper`) Actor

Scrape Immowelt.de (German real estate portal) from a search-results URL you paste. Returns title, price, rooms, living space m², location, provider/agent contact, images, and listing URL.

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

## Pricing

from $1.00 / 1,000 immowelt scraper — germany real estate listings | $1.19/1ks

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

## Immowelt Listings Scraper

Scrape **Immowelt.de** (German real estate portal) from a search-results URL you paste — no need to configure filters in this actor, just set them on immowelt.de and copy the resulting URL.

### Features

- Scrape any Immowelt search (buy or rent, any city/price/room filter you set on their site)
- Returns per-listing: price, rooms, living space m², location, provider/agent contact, images, and listing URL
- Uses Immowelt's own embedded Next.js `__NEXT_DATA__` search-results JSON — no HTML scraping, no browser
- **Pay per result** — you only pay for listing records actually extracted
- Automatic pagination up to `maxItems`, with residential-proxy session rotation on a block

### Input

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `searchUrl` | string | *(required)* | An immowelt.de search-results URL. Set your filters on immowelt.de, then copy the URL from your browser. |
| `maxItems` | integer | 100 | Max listings to return (0 = unlimited) |
| `proxyConfiguration` | proxy | RESIDENTIAL DE | Apify Residential proxy, Germany |

#### Why a search URL instead of filter fields?

Traction-proven Store competitors for Immowelt (`igolaizola/immowelt-scraper`, `azzouzana/immowelt-scraper`, `ninhothedev/immowelt-scraper`) mostly accept either a location/locationID pair or a raw search URL. This actor takes the URL directly — simplest, safest contract, and it avoids guessing Immowelt's internal location-ID scheme wrong.

### Output fields

| Field | Description |
|-------|-------------|
| `listing_id` | Immowelt internal listing/online ID |
| `url` | Direct URL to the listing |
| `title` / `description` | |
| `price` / `price_display` / `price_type` | `price_type` is best-effort (`kaufpreis`/`kaltmiete`/`warmmiete`) |
| `rooms` / `living_space_m2` / `plot_size_m2` | |
| `city` / `district` / `postal_code` / `street` / `country` | |
| `latitude` / `longitude` | |
| `provider_name` / `provider_phone` / `provider_type` | Listing agent/agency contact |
| `images` / `image_count` | |
| `energy_info` / `building_state` | Best-effort; full energy certificate detail lives on the classified detail page |
| `publication_date` / `update_date` | |
| `page_number` / `source` / `parse_confidence` / `warnings` / `scraped_at` | Provenance/QA fields. `source` is `next_data` or `ufrn_fetcher` depending on which embedded blob supplied the record. |

### Access path

Immowelt has no public search API. It's a Next.js SPA — the search-results page embeds the result set as JSON, primarily via `<script id="__NEXT_DATA__" type="application/json">` (standard Next.js page-data, parsed directly, no unescaping) at documented path `props.pageProps.classifiedsData`. This actor fetches the page with `curl_cffi` (Chrome TLS impersonation, no browser). As a fallback (only tried when `__NEXT_DATA__` yields nothing), it also decodes a second observed bootstrap blob, `window["__UFRN_FETCHER__"] = JSON.parse("...")`, which uses the same double-encoding scheme as the sibling `seloger-listings-scraper` actor's `initialData` — the decode routine is the same shape, ported here rather than shared, per this fleet's per-actor `_shared/` convention.

Apify **RESIDENTIAL (DE) proxy is recommended** for production reliability; third-party vendors document general anti-scraping blocking on this target though the specific mechanism (Cloudflare/DataDome/other) wasn't confirmed against a live run for this build.

### Honest caveats (read before relying on this in production)

- **Field-name mapping is unverified against a live Immowelt response.** The field names used here (`hardFacts.facts[]`, `hardFacts.price`, `location.address`, `gallery.images`, `provider`, `metadata.publishDate`, …) come from ScrapFly's public engineering write-up on scraping Immowelt.de, not from an authenticated cloud run against the live site (out of scope for this build). `normalize.py` is defensive: every field falls back to `None` and deducts `parse_confidence` rather than raising. Spot-check the exact field set against a real cloud run (with RESIDENTIAL DE proxy) before publishing or relying on this for paid delivery.
- **The exact `pageProps` wrapper key for the listings array is unconfirmed** — the parser tries several candidate keys (`classifiedsData`, `classifieds`, `searchResult`, `initialGeoSearchResult`) then falls back to a tolerant structural walk (first list of dicts with an `id` plus a signal field), same pattern as the sibling `seloger-listings-scraper` and the fleet's existing `immoscout24-listings` actor.
- **`hardFacts.facts[]` label matching (rooms/living-space/plot-size) is heuristic** (substring match on German/English label text), not a confirmed fixed key list — a mislabeled fact silently stays unmapped rather than crashing, which trades some recall for robustness under an unconfirmed schema.
- **Full energy certificate detail, exact address (house number), and construction-year data live on the classified detail page** (`props.pageProps.classified` per the same source), not the search-results payload — this actor is search-summary-only; detail-page enrichment is a straightforward v2 addition once the search path is verified live.
- Termination is by empty-page detection (30 results/page per source docs), not a parsed `totalPages` field, since the exact count-field name is also unconfirmed.

### Tests

Unit tests (`tests/`) run entirely offline against synthetic fixtures — no live network calls: `cd actors/REAL_ESTATE/immowelt-listings-scraper && python3 -m pytest -q`.

# Actor input Schema

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

Paste an immowelt.de search-results URL (go to immowelt.de, set your location/price/rooms/property-type filters, then copy the resulting URL from your browser's address bar, e.g. 'https://www.immowelt.de/liste/berlin/wohnungen/kaufen'). Filters are set on Immowelt's own site, not in this actor's input.

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

Maximum number of listings to return. Set to 0 for all available results on the search. Default 100.

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

Recommended: Apify Residential proxy with country Germany (DE) for best reliability. Without proxy, direct datacenter IPs may be blocked.

## Actor input object example

```json
{
  "searchUrl": "https://www.immowelt.de/classified-search?distributionTypes=Buy&estateTypes=Apartment&locations=AD08DE8634",
  "maxItems": 100,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "DE"
  }
}
```

# Actor output Schema

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

Dataset containing Immowelt Listings Scraper records (title, price, rooms, living\_space\_m2, city, postal\_code, provider\_name, image\_count, url, scraped\_at).

# 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 = {
    "searchUrl": "https://www.immowelt.de/classified-search?distributionTypes=Buy&estateTypes=Apartment&locations=AD08DE8634",
    "maxItems": 100,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "DE"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("bovi/immowelt-listings-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 = {
    "searchUrl": "https://www.immowelt.de/classified-search?distributionTypes=Buy&estateTypes=Apartment&locations=AD08DE8634",
    "maxItems": 100,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "DE",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("bovi/immowelt-listings-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 '{
  "searchUrl": "https://www.immowelt.de/classified-search?distributionTypes=Buy&estateTypes=Apartment&locations=AD08DE8634",
  "maxItems": 100,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "DE"
  }
}' |
apify call bovi/immowelt-listings-scraper --silent --output-dataset

```

## MCP server setup

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