# Immoweb Scraper & New-Listing Monitor - Belgium Property (`neverempty/immoweb-listings`) Actor

Turn monitoring on and every later run returns only the listings it has not returned before, so you are never charged for the same property twice. Every Immoweb.be listing as clean JSON: price with its meaning, monthly charges apart from rent, bedrooms, living and land area, EPC. Unofficial.

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

## Pricing

from $0.88 / 1,000 property returneds

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/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 - Belgium Real Estate Data, Price & Area

Pull property listings from **Immoweb.be** — Belgium's largest property portal — as clean JSON: the price as a number and what it means, the monthly charges kept apart from the rent, bedrooms, living and land area, EPC class, the full address with coordinates and the agency, for sale or to rent, anywhere in Belgium.

*Unofficial. This Actor is not affiliated with, endorsed by, or sponsored by Immoweb. All product names are trademarks of their respective owners.*

No API key. Reads the same public pages a visitor sees.

***

### Run it tomorrow and you only get what is new

Schedule this Actor with `monitoringMode: true` and each run returns **only the listings it has not returned before**. Not "here is everything again, please de-duplicate it yourself" — the Actor remembers what it already gave you, so a daily feed of a 4,000-listing search costs you the handful of new homes, not 4,000 rows every morning. **You are never charged for the same property twice.**

Listings your filters removed are remembered too, so widening a filter later does not resurrect them as false "new" listings. Listings that did not fit under your limit are **not** remembered, so they are still waiting on the next run instead of being silently lost.

***

### Why this one is different

#### The monthly charges are not sold to you as rent

Immoweb's rental cards carry two numbers: the requested monthly rent and, on most of them (29 of the 30 rentals measured), an additional monthly amount. That second figure is the *charges* (common costs), median **€190** against a median rent of **€1,580**. Adding them together, or passing the second one off as rent, would be wrong in either direction, so they come back apart:

- `price` + `priceType: "monthly-rent"` — the rent Immoweb asks for
- `monthlyCharges` — the charges, in their own column, only on rentals

Sales come back as `priceType: "sale"` with the total in `price`.

#### A new-build project is a range, not a zero

Grouped new-build listings (5 of the 90 measured) carry no single price — only a minimum and a maximum. They come back as `priceType: "sale-range"` with `price: null`, `priceMin` and `priceMax`, and Immoweb's own display text in `priceDisplay`. A price filter keeps a project if any part of its range fits.

#### A place Immoweb does not know is reported, not silently widened

Immoweb answers an unknown locality, an unknown postcode, or a property-type word it does not use with **HTTP 200 and a full page of houses from the whole of Belgium**. Read naively, that is a successful search. This Actor reads the place and the property type from the page title, and when they do not match what was asked for it returns one `no-such-search` row that explains how Immoweb spells its addresses — instead of charging you for 30 properties from the wrong end of the country. Property-type words are also checked before any request is made. A search that really has zero results is a different row (`no-results`); both are free.

#### Every row says where Immoweb actually searched

`searchedLocation` carries the place from Immoweb's own page title — `Brussels City (1000)`, `Brussels (Province)`, `Belgium` — so a locality that Immoweb widened to its province is visible in the data.

#### No private sellers' names

Immoweb's card data carries the advertiser's name with no flag saying whether it is an agency or a private person. This Actor returns the name **only when the advertiser has an agency logo** (all 90 listings measured did); a listing without one comes back with `agencyName: null`. Belgian phone numbers and e-mail addresses written into the title or address are removed. Property data, not a contact list.

#### One request, 30 complete properties

Immoweb embeds every card's full data in the list page. This Actor reads all 30 from that single page and **never opens individual property pages**. A listing that repeats across pages is dropped before delivery, so you are never charged twice for the same property. The list card carries at most four photos, so no photo count is claimed - only `mainImageUrl`.

#### Filters run here, because Immoweb's robots.txt says so

Immoweb's `robots.txt` disallows every query-string filter on its search pages (`minprice`, `maxprice`, `orderBy`, `countries`, `searchByRooms` …). This Actor therefore requests only the plain path plus `?page=N`, and every filter — price, bedrooms, living and land area, subtype, postcode, province, EPC class, under-option, new construction, keywords, agency, last-updated date — runs on the rows already fetched. Nothing extra is requested and nothing extra is charged. A property is never dropped for a value Immoweb did not publish. Sorting cannot be requested either; rows come in Immoweb's own order.

***

### Input

Either paste a search URL, or build one from the fields below.

| Field | Type | Default | Description |
|---|---|---|---|
| `searchUrl` | string | — | e.g. `https://www.immoweb.be/en/search/house/for-sale/brussels/1000`. Query parameters are dropped |
| `channel` | string | `sale` | `sale` or `rent` |
| `propertyType` | string | `house-and-apartment` | `house`, `apartment`, `house-and-apartment`, `garage`, `office`, `land`, `industry`, `business`, `castle` — Immoweb's own words |
| `locality` | string | — | `brussels`, `antwerp`, `gent`, `liege` … Empty = all of Belgium. On its own Immoweb takes it as the province (`brussels/province`) and this Actor follows that redirect for every page |
| `postalCode` | string | — | 4 digits, with the locality (`brussels` + `1000`). Ignored by Immoweb on its own |
| `maxListings` | integer | 20 | 1-1000. One property = one row = one charged event |
| `maxPages` | integer | 0 | Hard cap on requests; 0 = decide from `maxListings` |
| `useProxy` | boolean | `false` | Off by default — Immoweb answers Apify's network directly. Switches on by itself if blocked |
| `keywords` / `keywordMatch` / `excludeKeywords` | array / string / array | — | Match on title, type, address, flags, project and agency |
| `agencyNames` | array | — | Keep only these agencies (partial match) |
| `minPrice` / `maxPrice` | integer | 0 | EUR. Sale total or monthly rent; a project is kept if any part of its range fits |
| `requirePrice` | boolean | `false` | Drop listings with no single published price (a new-build range counts as none) |
| `minBedrooms` / `maxBedrooms` | integer | -1 | -1 = off. **An unpublished count is kept** |
| `minLivingAreaSqm` / `maxLivingAreaSqm` | integer | 0 | m². **An unpublished area is kept** |
| `minLandAreaSqm` | integer | 0 | m² |
| `propertySubtypes` | array | — | `VILLA`, `BUNGALOW`, `DUPLEX`, `PENTHOUSE`, `FLAT_STUDIO`, `GROUND_FLOOR`, `LOFT`, `MANSION`, `APARTMENT_BLOCK` … |
| `postalCodes` / `provinces` | array | — | Keep only these postcodes or provinces (as Immoweb names them) |
| `epcCertificates` | array | — | `A+` … `G`. **An unpublished class is kept** |
| `excludeUnderOption` | boolean | `false` | Drop listings flagged *under option* |
| `newConstructionOnly` | boolean | `false` | Keep only new construction / new projects |
| `excludeProjects` | boolean | `false` | Drop grouped projects that carry a price range |
| `updatedWithinDays` | integer | 0 | By Immoweb's last-modification date. 0 = off |
| `monitoringMode` | boolean | `false` | Return only properties not returned on a previous run |
| `resetMonitoringState` | boolean | `false` | One-shot: forget what was already returned |

```json
{ "channel": "sale", "propertyType": "house", "locality": "gent", "postalCode": "9000", "maxListings": 100,
  "minBedrooms": 3, "maxPrice": 450000, "minLivingAreaSqm": 120, "epcCertificates": ["A", "B", "C"], "excludeUnderOption": true }
```

#### Monitoring mode turns this into a daily feed

Schedule it and set `monitoringMode: true`: each run returns **only the properties it has not returned before**. Properties removed by your filters are remembered too, so changing a filter later does not resurrect them as false "new" listings — and properties that simply **did not fit under `maxListings`, or were cut off by the run's charge limit, are not remembered**, so they are still waiting for you on the next run rather than being silently lost.

Measured on 2026-09-05: **48,659** houses for sale across Belgium; **749** apartments to rent in Brussels City (1000).

***

### Output

One row per property:

```json
{
  "source": "immoweb.be",
  "status": "ok",
  "listingId": "21814826",
  "title": "Flémalle, 4 chs, terrasse, jardin, élec conf",
  "url": "https://www.immoweb.be/en/classified/21814826",
  "searchedLocation": "Belgium",
  "transactionType": "sale",
  "propertyType": "HOUSE",
  "propertySubtype": "HOUSE",
  "bedrooms": 4,
  "roomCount": null,
  "livingAreaSqm": 152,
  "landAreaSqm": 195,
  "floor": null,
  "price": 199000,
  "priceCurrency": "EUR",
  "priceType": "sale",
  "priceDisplay": "€199,000",
  "priceMin": null,
  "priceMax": null,
  "isPriceHidden": false,
  "hasStartingPrice": true,
  "monthlyCharges": null,
  "epcCertificate": "C",
  "street": "Grand'Route",
  "houseNumber": "445",
  "postalCode": "4400",
  "locality": "Flémalle",
  "district": "Liège",
  "province": "Liège",
  "region": "Wallonie",
  "country": "Belgium",
  "placeName": "Liège environs",
  "latitude": 50.5939295,
  "longitude": 5.456208,
  "listingFlag": "new",
  "isNewListing": true,
  "isUnderOption": false,
  "secondaryFlags": [],
  "isNewConstruction": false,
  "projectName": null,
  "projectSoldPercent": null,
  "agencyName": "Nigel Immo",
  "mainImageUrl": "https://media-resize.immowebstatic.be/classifieds/…/736x736/….jpg",
  "hasVirtualTour": true,
  "lastModifiedAt": "2026-09-04T11:04:56.740Z",
  "scrapedAt": "2026-09-05T07:50:35.742Z"
}
```

Measured across 90 live listings on 2026-09-05: id, title, url, type, subtype, street, postcode, locality, province, region, coordinates, listing flag, agency and last-modified date on **90 of 90**; house number on **89**; a single price on **85** (the other 5 are project ranges); bedrooms on **85**; living area on **85**; land area on **55**; floor on **30**; EPC class on **19**; monthly charges on **29 of the 30 rentals**. Immoweb does not publish a first-listed date on the list page, so `lastModifiedAt` is the only date.

#### Rows that are never charged

| `status` | when |
|---|---|
| `no-results` | Immoweb reports zero properties for this search (its title carries no count). That is its answer, not a failure |
| `no-such-search` | Immoweb does not know the locality, postcode or property type and silently widened the search to all houses in Belgium. **Not** a claim that there is nothing there |
| `no-filter-match` | Listings were read, but your filters removed all of them. The row says how many were read |
| `no-new-listings` | Monitoring mode: nothing new since the previous run |
| `unreadable` | The page could not be read (Immoweb answers a page beyond the last with HTTP 500), its cards were missing, or its title claimed a count while yielding nothing |

***

### Pricing

Pay per property returned. Rows that report an empty search, an unknown location or type, a filter that matched nothing, or a page that could not be read are **not** charged. A property that appears twice across pages is dropped before delivery, so it is never charged twice.

***

### Notes

- `robots.txt` was read in full on 2026-09-05. The `User-agent: *` section disallows profile, ad-placement, print and map pages, advanced search, and **every search query parameter it names** (`countries`, `orderBy`, `minprice`, `maxprice`, `searchByRooms`, `searchId`, `isCheap`, `ln`, `translate`). This Actor requests only `/en/search/<type>/<for-sale|for-rent>[/<locality>[/<postcode>]]` with `?page=N`.
- Only public pages are read. No login, no API key.

# Actor input Schema

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

Paste an English Immoweb listing page, for example https://www.immoweb.be/en/search/house/for-sale/brussels/1000 or https://www.immoweb.be/en/search/apartment/for-rent, and the four fields below are ignored. Query parameters are dropped: Immoweb's robots.txt forbids them, so every filter runs here on the rows already fetched.

## `channel` (type: `string`):

sale = for-sale, rent = for-rent.

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

Immoweb's own words. Any other word is silently turned into houses across Belgium by Immoweb, so only these are accepted.

## `locality` (type: `string`):

A town as Immoweb spells it in its addresses: brussels, antwerp, gent, liege, leuven. Leave empty for the whole of Belgium. A locality without a postcode is taken by Immoweb as the whole province. A locality Immoweb does not know is reported as a no-such-search row, not as an empty result - Immoweb would otherwise silently widen the search to the whole country.

## `postalCode` (type: `string`):

The 4-digit Belgian postcode that goes with the locality (brussels + 1000, antwerp + 2000). Immoweb ignores a postcode on its own, so it needs the locality.

## `maxListings` (type: `integer`):

How many properties to return. You are charged for the rows you actually receive.

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

A hard stop on how many pages are requested, whatever the filters do. 0 lets the run work it out.

## `useProxy` (type: `boolean`):

Off by default because Immoweb answers Apify's own network directly. If Immoweb ever starts blocking, the run switches to a proxy on its own.

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

Keep only properties whose title, type, address, flags, project or agency contain these words. No extra requests are made.

## `keywordMatch` (type: `string`):

any: at least one keyword. all: every keyword.

## `excludeKeywords` (type: `array`):

Drop properties containing any of these words.

## `agencyNames` (type: `array`):

Keep only properties marketed by these agencies. Partial names work.

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

A sale price is a total; a rent is per month, the way Immoweb publishes it. A new-build project with a price range is kept if any part of the range fits. 0 means no minimum.

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

0 means no maximum. Monthly charges are a separate column and are not included.

## `requirePrice` (type: `boolean`):

Off by default: a property whose price is not published, or is only a range, is simply kept.

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

-1 turns the filter off.

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

-1 turns the filter off.

## `minLivingAreaSqm` (type: `integer`):

0 means no minimum. A property without a published area is kept.

## `maxLivingAreaSqm` (type: `integer`):

0 means no maximum.

## `minLandAreaSqm` (type: `integer`):

0 means no minimum. A property without a published land area is kept.

## `propertySubtypes` (type: `array`):

Immoweb's own subtype words, for example VILLA, BUNGALOW, DUPLEX, PENTHOUSE, FLAT\_STUDIO, GROUND\_FLOOR, LOFT, MANSION, APARTMENT\_BLOCK, MIXED\_USE\_BUILDING. Leave empty for all.

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

Keep only these 4-digit postcodes. Useful with a province-wide search.

## `provinces` (type: `array`):

Keep only these provinces as Immoweb names them: Brussels, Antwerp, East Flanders, West Flanders, Flemish Brabant, Limburg, Walloon Brabant, Hainaut, Liège, Luxembourg, Namur.

## `epcCertificates` (type: `array`):

Keep only these energy classes (A+, A, B, C, D, E, F, G). A property without a published class is kept.

## `excludeUnderOption` (type: `boolean`):

Drop listings Immoweb flags as under option (an offer has been accepted).

## `newConstructionOnly` (type: `boolean`):

Keep only listings Immoweb flags as new construction or a new real-estate project.

## `excludeProjects` (type: `boolean`):

Drop the grouped project listings that carry a min-max price instead of one price.

## `updatedWithinDays` (type: `integer`):

By Immoweb's last-modification date (Immoweb does not publish the first-listed date on the list page). 0 means no date filter. A property whose date cannot be read is kept.

## `monitoringMode` (type: `boolean`):

Turn this on and schedule the Actor: each run returns only the properties it has never returned before, so you are charged for new stock rather than for the same pages again. Properties that did not fit under the limit are still waiting for you on the next run.

## `resetMonitoringState` (type: `boolean`):

Clears the memory for this search, so the next monitoring run starts from scratch. Works with or without monitoring mode on.

## Actor input object example

```json
{
  "channel": "sale",
  "propertyType": "house-and-apartment",
  "locality": "",
  "postalCode": "",
  "maxListings": 20,
  "maxPages": 0,
  "useProxy": false,
  "keywords": [],
  "keywordMatch": "any",
  "excludeKeywords": [],
  "agencyNames": [],
  "minPrice": 0,
  "maxPrice": 0,
  "requirePrice": false,
  "minBedrooms": -1,
  "maxBedrooms": -1,
  "minLivingAreaSqm": 0,
  "maxLivingAreaSqm": 0,
  "minLandAreaSqm": 0,
  "propertySubtypes": [],
  "postalCodes": [],
  "provinces": [],
  "epcCertificates": [],
  "excludeUnderOption": false,
  "newConstructionOnly": false,
  "excludeProjects": false,
  "updatedWithinDays": 0,
  "monitoringMode": false,
  "resetMonitoringState": false
}
```

# Actor output Schema

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

One row per property: title, the price as a number with what it means (a sale total, a monthly rent, or a new-build price range kept as min and max), the monthly charges in their own column, EPC class, bedrooms, living and land area, floor, the full address with postcode, province, region and coordinates, Immoweb's listing flags (new, under option, new construction), the project name where it is one, the agency, the main image and the last-modified date. Every row also names the place Immoweb actually searched. Searches with no results, locations or property types Immoweb does not know, filters that matched nothing, and pages that could not be read come back as their own rows and are not charged.

# 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("neverempty/immoweb-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 = {}

# Run the Actor and wait for it to finish
run = client.actor("neverempty/immoweb-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 '{}' |
apify call neverempty/immoweb-listings --silent --output-dataset

```

## MCP server setup

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