# Immoweb Property Scraper (Belgium) (`scrapyx/immoweb-properties-scraper`) Actor

Scrapes houses, apartments, land and commercial property from Immoweb — Belgium's #1 real-estate portal. Filter by type, postal code, price and bedrooms; returns price, address, surface, energy certificate, amenities, photos and agency contact for every listing.

- **URL**: https://apify.com/scrapyx/immoweb-properties-scraper.md
- **Developed by:** [Ibnu Adzim](https://apify.com/scrapyx) (community)
- **Categories:** Real estate
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.84 / 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.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Immoweb Property Scraper (Belgium)

Scrapes houses, apartments, land, offices and commercial property from
**[Immoweb](https://www.immoweb.be)** — Belgium's #1 real-estate portal, with
roughly 81,000 houses and apartments listed for sale at any time.

Public data only. No login, no cookies, no browser — a plain HTTP client
talking to the same JSON endpoint Immoweb's own website uses.

### What you get

Two record types share one dataset, told apart by `recordType`:

#### `PROPERTY` — one row per listing

Search rows carry price, address, surface, bedrooms, photos, energy
certificate and the listing agency. With **Fetch full property details** on
(the default), each row also gets a `propertyDetails` object with:

- the complete description text (all languages the agent supplied)
- every amenity flag — garden, terrace, lift, basement, attic, air-con,
  alarm, fireplace, jacuzzi, sauna, disabled access, …
- energy: EPC score, certificate, heating type, insulation, glazing
- surfaces broken out — habitable, land, garden, terrace, per-room
- building: construction year, condition, number of floors, façade count
- full media set — photos at four resolutions, floor plans, virtual tour,
  video
- legal documents and the agency's contact block

#### `SEARCH_SUMMARY` — one row per search

Upstream's own match total, how deep the run paged, whether the query hit
Immoweb's ceiling, and — importantly — **which of your filters Immoweb
actually applied** (see the honesty note below).

### Input

| Field | What it does |
| --- | --- |
| **Property type** | house, apartment, land, office, garage, commercial, industrial, plus subtypes (villa, castle, penthouse, loft, duplex, kot…) |
| **For sale / for rent** | the two transaction states Immoweb exposes publicly |
| **Country** | Belgium (where the inventory is) plus FR/NL/LU/ES/IT |
| **Postal codes** | e.g. `1000` Brussels, `2000` Antwerp, `9000` Ghent, `4000` Liège |
| **Price / bedrooms / land surface** | min–max ranges |
| **Sort order** | relevance, newest, cheapest, most expensive, postcode, surface |
| **Classified URLs** | scrape specific listings directly, skipping search |
| **Max properties per search** | `0` = unlimited (up to Immoweb's own 9,990 ceiling) |
| **Fetch full property details** | off = fast listing-only crawl, 1 request per 30 properties |

#### Example

```json
{
  "propertyType": "house",
  "transactionType": "for-sale",
  "country": "BE",
  "postalCodes": ["1000", "1050"],
  "minPrice": 250000,
  "maxPrice": 600000,
  "minBedrooms": 3,
  "maxItems": 500,
  "includePropertyDetails": true
}
```

### Two things this actor is honest about

**1. Immoweb silently ignores filters it does not understand.** Ask for postal
code `9999` and it answers HTTP 200 with all 47,777 nationwide houses — not an
error, just quietly the wrong data. This actor reads back the filter set
Immoweb reports it actually honoured and puts it on every `SEARCH_SUMMARY` row:

```json
"filtersApplied":    {"postalCodes": ["1000"], "postalCodeLabels": ["Brussels City (1000)"]},
"filtersIgnored":    [],
"allFiltersApplied": true
```

If something was dropped, `filtersIgnored` names it and the run logs a warning.
Property types are checked against a verified allowlist *before* the first
request, so a typo is refused rather than answered with unfiltered results.

**2. Immoweb caps pagination at 9,990 results per query** (333 pages × 30),
even when it reports far more matches. When a run hits that ceiling,
`resultCapReached: true` appears on the summary row and the log tells you to
narrow by postcode or price. No actor can page past it — the endpoint returns
HTTP 500 at page 334.

### Notes on reliability

- **No WAF challenge.** Immoweb sits behind Cloudflare but serves data cold on
  every TLS profile tested. Nothing to solve, nothing to refresh.
- **Runs unattended.** No human-captured session, no expiring token — safe on
  a schedule.
- **Residential proxy by default** for cloud runs. Container egress is a
  different posture than a home connection, and this portfolio has lost a full
  cloud run to that difference before.
- **Failures never vanish.** A listing that sells mid-crawl degrades that one
  row (`_detailError`) instead of failing the run; every input maps to at least
  one output row.

### Output envelope

Every record carries `_input`, `_source` and `_scrapedAt`. Upstream field names
are passed through **verbatim** — no renaming — so downstream schema drift has
exactly one place to fix. `_source` is `S1-search-json` for listing-only rows
and `S1-search-json+S2-classified-var` once details are attached.

See [`CRAWLING_METHOD.md`](CRAWLING_METHOD.md) for the full reverse-engineering
trail, the pagination ceiling bisection, and the filter-trap evidence.

# Actor input Schema

## `propertyType` (type: `string`):

Which kind of property to search. Only the values in this list are real Immoweb path segments — an unrecognised one is answered with HTTP 200 and silently returns the unfiltered house/for-sale results, so anything else is refused rather than run. Subtypes (villa, castle, penthouse…) narrow within their parent type.

## `transactionType` (type: `string`):

Only these two transaction states are reachable through Immoweb's public search. 'sold' and 'rented' archives are not exposed.

## `country` (type: `string`):

Immoweb is a Belgian portal but also lists foreign property. Belgium is where the inventory actually is (≈48k houses for sale); the others are thin.

## `postalCodes` (type: `array`):

Belgian postal codes to restrict the search to, e.g. 1000 (Brussels City), 2000 (Antwerp), 9000 (Ghent), 4000 (Liège). Leave empty for the whole country. Every code is checked against what Immoweb reports back as actually applied — an unrecognised code is reported on the SEARCH\_SUMMARY row as `filtersIgnored` instead of silently returning nationwide results.

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

Leave empty or 0 for no minimum.

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

Leave empty or 0 for no maximum.

## `minBedrooms` (type: `integer`):

Leave empty or 0 for any.

## `maxBedrooms` (type: `integer`):

Leave empty or 0 for any.

## `minLandSurface` (type: `integer`):

Leave empty or 0 for any. Applies to the plot, not the habitable area.

## `orderBy` (type: `string`):

How Immoweb should order the results before pagination.

## `classifiedUrls` (type: `array`):

Scrape specific listings directly, skipping search. Accepts a full URL (https://www.immoweb.be/en/classified/21761641) or a bare numeric id. Can be combined with a search in the same run.

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

Stop paginating after this many properties. Set to 0 for unlimited — note that Immoweb itself serves at most 9,990 results per query (333 pages × 30) no matter how many matches exist, so unlimited means 'up to 9,990'. Narrow by postal code or price to reach the rest.

## `includePropertyDetails` (type: `boolean`):

Fetch the complete listing (full description, every amenity flag, energy certificate, floor plans, legal documents, agency contact) for each result via one extra request per property. Turn this off for a fast listing-only crawl — search rows already carry price, address, surface, bedrooms, photos and the agency name.

## `maxConcurrency` (type: `integer`):

Upper bound on requests in flight at once, across both search pagination and detail fetches. Immoweb is not rate-limit sensitive at these levels, but keep it modest to stay a good citizen.

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

Immoweb runs no active bot challenge and a direct connection works, but Residential is the default for cloud runs — datacenter egress from the Apify container is a different posture than a local ISP connection, and this portfolio has already lost a full cloud run to that difference once.

## Actor input object example

```json
{
  "propertyType": "house",
  "transactionType": "for-sale",
  "country": "BE",
  "postalCodes": [],
  "minPrice": 0,
  "maxPrice": 0,
  "minBedrooms": 0,
  "maxBedrooms": 0,
  "minLandSurface": 0,
  "orderBy": "relevance",
  "classifiedUrls": [],
  "maxItems": 100,
  "includePropertyDetails": true,
  "maxConcurrency": 4,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `items` (type: `string`):

One row per scraped record. See the dataset's default view for field definitions.

# 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("scrapyx/immoweb-properties-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("scrapyx/immoweb-properties-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 scrapyx/immoweb-properties-scraper --silent --output-dataset

```

## MCP server setup

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