# Immoweb Scraper | Belgium Property Listings & EUR per m2 (`alinz/immoweb-belgium-property`) Actor

Belgian property listings from Immoweb with price per square metre computed, multi-unit developments and life-annuity sales flagged so they never distort a comparable, and median EUR/m2 by postcode. Search by postcode, price, bedrooms, surface and property subtype.

- **URL**: https://apify.com/alinz/immoweb-belgium-property.md
- **Developed by:** [אלון זריהן](https://apify.com/alinz) (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 $2.00 / 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/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

## Immoweb Scraper — Belgian Property Listings with €/m²

Property listings from **Immoweb**, Belgium's dominant property portal, with the price per square metre computed and the listings that would corrupt a comparison flagged and excluded.

### What you get

| | |
|---|---|
| Coverage | All of Belgium, or filtered to specific postcodes |
| Markets | For sale and for rent |
| Types | Houses, apartments, land, garages, offices, business premises |
| Per listing | Price, **€/m²**, surface, bedrooms, subtype, address, coordinates, agency, photos |
| Statistics | Median and quartile €/m² by postcode and subtype |

### Three things this does that a raw listing dump does not

**1. It computes €/m².** Immoweb leaves the `pricePerSqm` field empty on every listing. This Actor derives it from price and net habitable surface, and only where a real surface exists — so the figure is never fabricated from a missing value.

**2. It flags what is not comparable.** Roughly 13% of listings are multi-unit developments (`APARTMENT_GROUP`, `HOUSE_GROUP`) that quote a *price range* and have no single surface. Others are life annuities (viager), public sales, or starting-price listings. Mixed into an average, each of these distorts it. They are labelled, excluded by default, and available with one setting when you do want them.

**3. It merges the same town under its different names.** Belgium is trilingual and agencies type whatever they like, so one commune arrives as `Bruxelles`, `BRUSSEL`, `Brussels` and `BRUXELLES` in a single result set. Grouping on the name would give you four partial medians for one market. Statistics group by **postcode** instead, and every spelling seen is listed in `localityVariants` so the merge is visible rather than hidden.

Measured across 112 postcodes in live results, 7 carried more than one spelling — and in every case they were the same municipality.

### Output

**`listings`** — one row per property.

```json
{
  "listingId": 21800810,
  "url": "https://www.immoweb.be/en/classified/21800810",
  "price": 395000,
  "priceDisplay": "€395,000",
  "pricePerSqm": 3211,
  "propertyType": "APARTMENT",
  "propertySubtype": "APARTMENT",
  "title": "Superb 1-bedroom apartment",
  "bedroomCount": 1,
  "habitableSurface": 123,
  "address": "Rue Royale 182",
  "postalCode": "1000",
  "locality": "Bruxelles",
  "province": "Brussels",
  "region": "Brussels",
  "latitude": 50.8515465,
  "longitude": 4.3646488,
  "agencyName": "Property Lab",
  "isNew": true,
  "isDevelopment": false,
  "isLifeAnnuity": false,
  "isStandardSale": true,
  "pictureCount": 4,
  "lastModified": "2026-08-31T09:05:00.873Z"
}
```

**`stats`** — median €/m² per postcode and subtype.

```json
{
  "postalCode": "1000",
  "locality": "Bruxelles",
  "localityVariants": ["Bruxelles", "BRUSSEL", "BRUXELLES"],
  "propertySubtype": "PENTHOUSE",
  "listingCount": 6,
  "medianPricePerSqm": 6633,
  "p25PricePerSqm": 5800,
  "p75PricePerSqm": 7400,
  "medianPrice": 895000,
  "medianSurface": 135
}
```

### Examples

**Brussels apartments in a price band:**

```json
{ "postalCodes": ["1000", "1050", "1180"], "maxPages": 10, "minPrice": 250000, "maxPrice": 750000 }
```

**Value screen — anything under €3,000/m²:**

```json
{ "maxPages": 30, "maxPricePerSqm": 3000, "minSurface": 70 }
```

**Median €/m² across the major cities:**

```json
{
  "postalCodes": ["1000", "2000", "9000", "8000", "4000"],
  "maxPages": 25,
  "outputFormat": "stats",
  "minListingsPerGroup": 5
}
```

**Villas and penthouses only:**

```json
{ "subtypes": ["VILLA", "PENTHOUSE"], "maxPages": 20 }
```

**Rental market:**

```json
{ "transaction": "rent", "postalCodes": ["1000"], "maxPages": 10 }
```

### Field reference

| Field | Meaning |
|---|---|
| `price` / `priceDisplay` | Asking price in EUR. Null for developments, which quote a range |
| `priceMin` / `priceMax` | The range, for multi-unit developments |
| `pricePerSqm` | Computed: price ÷ net habitable surface. Null when no surface is recorded |
| `propertyType` / `propertySubtype` | HOUSE, APARTMENT, and the subtype (VILLA, PENTHOUSE, DUPLEX, …) |
| `habitableSurface` / `landSurface` | Net habitable and land area in m² |
| `bedroomCount` / `roomCount` | As recorded by the agency |
| `postalCode` / `locality` / `district` / `province` / `region` | Location |
| `latitude` / `longitude` | WGS84 coordinates, ready to map |
| `agencyName` | Listing agency (business name only — no personal contact data is collected) |
| `isDevelopment` | Multi-unit project quoting a price range |
| `isLifeAnnuity` / `isPublicSale` / `hasStartingPrice` | Non-standard sale forms |
| `isStandardSale` | False if any of the above — excluded from comparables by default |
| `isNew`, `hasVideo`, `has360Tour`, `pictureCount`, `thumbnailUrl` | Listing presentation |
| `lastModified` | When the agency last updated the listing |

### Notes and limits

- **Price filters are applied after fetching, not by the site.** Immoweb's robots.txt disallows URLs containing `minprice`, `maxprice`, `orderBy`, `searchByRooms` and `searchId`, so this Actor does not use them. The consequence is practical: a narrow filter over few pages can legitimately return nothing. Raise `maxPages` rather than assuming it is broken.
- **There is no price-history signal.** The `oldPrice` field exists in Immoweb's data model but was empty on all 300 listings sampled, so no price-drop filter is offered. A filter that always returns zero is worse than no filter.
- **These are asking prices**, not recorded sale prices. For what Belgian property actually sold for, the notarial registers are the source, not a portal.
- **Bot protection.** Immoweb runs DataDome. This Actor crawls politely with a pause between pages and makes no attempt to evade detection — no proxy rotation, no fingerprint spoofing, no challenge solving. If a challenge is served it stops, returns what it has, and says so in the log. Raising `delayMs` helps.

### Conduct

Only `/en/search/` pages are read — paths that Immoweb's robots.txt permits — and the disallowed query parameters are never used. The data collected contains no personal information: no phone numbers, emails or individual names, only agency business names.

### Integrations

Output lands in a standard Apify dataset: export as **JSON, CSV, Excel or XML**, or connect to **Make, Zapier, n8n, Google Sheets** or your own code via the Apify API. Coordinates are included, so results map directly.

# Actor input Schema

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

Which listing category to crawl.

## `transaction` (type: `string`):

Whether to crawl properties for sale or for rent.

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

Optional. 1000 = central Brussels, 1050 = Ixelles, 1180 = Uccle, 2000 = Antwerp, 9000 = Ghent, 8000 = Bruges. Leave empty to crawl all of Belgium.

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

30 listings per page. Pages are fetched sequentially with a pause between them, so this is the main driver of run time.

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

Applied after fetching. Immoweb's robots.txt disallows price parameters in URLs, so this Actor filters locally instead of asking the site to filter.

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

Applied after fetching, for the same robots.txt reason as the minimum.

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

Applied after fetching. Listings with no bedroom count are excluded when this is set.

## `minSurface` (type: `integer`):

Applied after fetching. Listings with no recorded surface are excluded when this is set.

## `maxPricePerSqm` (type: `integer`):

Screen for value: only listings at or below this rate. Computed by this Actor, since Immoweb leaves the field empty.

## `standardSalesOnly` (type: `boolean`):

Excludes multi-unit developments, life annuities (viager), public sales and starting-price listings. Their headline prices are not comparable to ordinary sales and will distort any average.

## `subtypes` (type: `array`):

Optional. Filter to specific subtypes, e.g. VILLA, PENTHOUSE, DUPLEX, FLAT\_STUDIO, TOWN\_HOUSE, MANSION, EXCEPTIONAL\_PROPERTY.

## `outputFormat` (type: `string`):

Statistics group by postcode rather than town name, because one Belgian town appears under French, Dutch and English spellings at once.

## `minListingsPerGroup` (type: `integer`):

Statistics mode only. Groups smaller than this are dropped, since a median over two listings is noise.

## `maxResults` (type: `integer`):

Optional cap on rows returned. Useful for a cheap trial run.

## `delayMs` (type: `integer`):

Immoweb runs bot protection. This Actor crawls politely rather than trying to evade it; raise this if the site starts serving challenges.

## Actor input object example

```json
{
  "propertyType": "house-and-apartment",
  "transaction": "sale",
  "postalCodes": [
    "1000",
    "1050",
    "1180"
  ],
  "maxPages": 5,
  "minPrice": 250000,
  "maxPrice": 750000,
  "minBedrooms": 2,
  "minSurface": 60,
  "maxPricePerSqm": 4000,
  "standardSalesOnly": true,
  "subtypes": [
    "VILLA",
    "PENTHOUSE"
  ],
  "outputFormat": "listings",
  "minListingsPerGroup": 5,
  "maxResults": 100,
  "delayMs": 2500
}
```

# Actor output Schema

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

One row per listing, or one row per postcode and subtype in statistics mode.

## `listings` (type: `string`):

Price, price per m2, surface, bedrooms, location and agency for each property.

## `marketStats` (type: `string`):

Median and quartile EUR/m2 per postcode and property subtype.

## `runStats` (type: `string`):

Pages fetched, listings found, filters applied, and whether the site served a bot challenge.

# 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 = {
    "propertyType": "house-and-apartment",
    "transaction": "sale",
    "postalCodes": [
        "1000",
        "1050"
    ],
    "maxPages": 5,
    "subtypes": [],
    "outputFormat": "listings",
    "minListingsPerGroup": 5
};

// Run the Actor and wait for it to finish
const run = await client.actor("alinz/immoweb-belgium-property").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 = {
    "propertyType": "house-and-apartment",
    "transaction": "sale",
    "postalCodes": [
        "1000",
        "1050",
    ],
    "maxPages": 5,
    "subtypes": [],
    "outputFormat": "listings",
    "minListingsPerGroup": 5,
}

# Run the Actor and wait for it to finish
run = client.actor("alinz/immoweb-belgium-property").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 '{
  "propertyType": "house-and-apartment",
  "transaction": "sale",
  "postalCodes": [
    "1000",
    "1050"
  ],
  "maxPages": 5,
  "subtypes": [],
  "outputFormat": "listings",
  "minListingsPerGroup": 5
}' |
apify call alinz/immoweb-belgium-property --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,alinz/immoweb-belgium-property"
        }
    }
}

```

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/pNnbBLRwHAM7KPiuw/builds/NFx2hXASCNwO6RfST/openapi.json
