# atHome.lu Scraper - Luxembourg Real Estate Data & Prices (`neverempty/athome-listings`) Actor

atHome.lu listings for sale or rent as clean JSON: price as a number with its meaning (sale, monthly rent or new-build range), charges apart from rent, price per m2, surface, bedrooms, energy class, address with coordinates and the agency. No agent contacts. Unofficial.

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

## Pricing

from $7.30 / 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

## atHome.lu Scraper - Luxembourg Real Estate Data & Prices

Pull property listings from **atHome.lu** — Luxembourg's largest property portal — as clean JSON: the price as a number and what it means, price per m², surface, bedrooms, energy class, address with coordinates and the agency, for sale or to rent, across Luxembourg.

*Unofficial. This Actor is not affiliated with, endorsed by, or sponsored by atHome. 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 price means what it says, and a new-build project is a range

atHome keeps a sale total in one field and a monthly rent in another. This Actor labels the number: `priceType: "sale"` (median in the 40 Luxembourg-city flats for sale measured: **€1,000,000**) or `priceType: "monthly-rent"` (median in the 20 houses to rent: **€4,900**), with the monthly charges in their own `monthlyCharges` column when atHome publishes them on the list page (it did not for any of the 26 rentals measured) and atHome's own price per m² in `pricePerSqm` (€/m² for a sale, €/m² per month for a rental). A new-build project (5 of the 60 listings measured) carries no single price: it comes back as `priceType: "sale-range"` with `price: null`, `priceMin`, `priceMax` and the surface range. A confidential price stays `null` with `isPriceConfidential: true` — never a zero.

#### A project's bedroom count is not sold as one flat's bedrooms

On a project card atHome sums the bedrooms of every unit (a block with 7 or 10 "bedrooms"). This Actor returns `bedroomsMin` / `bedroomsMax` for projects and leaves `bedrooms`, `bathrooms` and `floor` empty rather than passing the sum off as one home. For a single home the floor comes from the card data atHome keeps in a second list on the same page (0 is the ground floor); the land is published in **ares** and is returned both as `landAres` and as `landSqm` (1 are = 100 m²). Flags the list page does not really carry — new-build, lift, furnished — are not returned at all, because every listing came back the same.

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

atHome answers an unknown town with **HTTP 200 and 20 listings from the whole country**, French border towns included. This Actor checks atHome's own `resolvedLocations`: when it is empty, one free `no-such-search` row explains how atHome spells its places instead of 20 charged listings from the wrong end of the country. `searchedLocation` on every row carries the place atHome actually resolved.

#### No agent e-mails, no phone numbers

atHome's card data carries the agency's e-mail and phone on **every one of the 60 listings measured**, and the named agent's e-mail and landline on 29. This Actor returns the agency name only; private sellers are not named at all; Luxembourg phone numbers and e-mail addresses written into the text are removed. Property data, not a contact list.

#### atHome's page is read, never executed

atHome embeds its data as a JavaScript object, not JSON. This Actor turns it into JSON by text replacement and parses it — it never evaluates atHome's code.

#### One request, 20 complete listings

atHome embeds every card's full data in the list page. This Actor reads all 20 from that single page and **never opens individual listing pages**. A listing that repeats across pages is dropped before delivery, so you are never charged twice.

#### Filters run here

Every filter — price, bedrooms, surface, subtype, postcode, city, energy class, projects, sold, advertiser, keywords, agency — runs on the rows already fetched. Nothing extra is requested from atHome and nothing extra is charged. A property is never dropped for a value atHome did not publish.

***

### Input

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

| Field | Type | Default | Description |
|---|---|---|---|
| `searchUrl` | string | — | e.g. `https://www.athome.lu/en/buy/apartment/luxembourg`. Query parameters are dropped |
| `channel` | string | `sale` | `sale` or `rent` |
| `propertyType` | string | `apartment` | `apartment` or `house` — atHome's words |
| `location` | string | `luxembourg` | A town as atHome spells it: `luxembourg`, `esch-sur-alzette`, `differdange`. Every row carries the place atHome resolved it to — `kirchberg`, for example, is a village in Germany on atHome, not the Luxembourg district |
| `maxListings` | integer | 20 | 1-1000. One listing = one row = one charged event |
| `maxPages` | integer | 0 | Hard cap on requests; 0 = decide from `maxListings` |
| `includeDescription` | boolean | `true` | The English description. The keyword filter searches it either way |
| `useProxy` | boolean | `false` | Off by default — atHome answers Apify's network directly. Switches on by itself if blocked |
| `keywords` / `keywordMatch` / `excludeKeywords` | array / string / array | — | Match on title, description, type, address, city, energy class 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 range counts as none) |
| `minBedrooms` / `maxBedrooms` | integer | -1 | -1 = off. **An unpublished count is kept** |
| `minSurfaceSqm` / `maxSurfaceSqm` | integer | 0 | m². **An unpublished surface is kept** |
| `propertyTypes` | array | — | Partial match: `Apartment`, `Penthouse`, `Duplex`, `Studio`, `House`, `Apartment block` … |
| `postalCodes` / `cities` | array | — | Keep only these postcodes or cities (partial match on the city) |
| `energyClasses` | array | — | `A` … `I`. **An unpublished class is kept** |
| `excludeProjects` | boolean | `false` | Drop new-build projects that carry a price range |
| `excludeSold` | boolean | `false` | Drop listings flagged sold or under agreement |
| `advertiserType` | string | `any` | `agency` or `private` |
| `monitoringMode` | boolean | `false` | Return only listings not returned on a previous run |
| `resetMonitoringState` | boolean | `false` | One-shot: forget what was already returned |

```json
{ "channel": "sale", "propertyType": "apartment", "location": "luxembourg", "maxListings": 100,
  "minBedrooms": 2, "maxPrice": 900000, "energyClasses": ["A", "B"], "excludeProjects": true }
```

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

Schedule it and set `monitoringMode: true`: each run returns **only the listings it has not returned before**. Listings removed by your filters are remembered too, so changing a filter later does not resurrect them as false "new" listings — and listings 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.

atHome does not sort by date: its list puts paid placements first, so a new listing can sit on any page. In monitoring mode this Actor therefore keeps reading — up to 60 pages, or `maxPages` if you set it — until it has found `maxListings` listings it has not returned before. A search wider than that (Luxembourg city has 131 pages of flats for sale) needs `maxPages` raised to be covered fully; reading pages is free, only returned listings are charged.

Measured on 2026-09-05: **2,618** flats for sale in Luxembourg city (131 pages); **24** houses to rent.

***

### Output

One row per listing:

```json
{
  "source": "athome.lu",
  "status": "ok",
  "listingId": "7900775",
  "title": "Résidences Mont Carmin & Mont Ruby à Luxembou",
  "description": "For sale - luxury apartments - Luxembourg-city …",
  "url": "https://www.athome.lu/en/buy/new-property/apartment-block/residences-mont-carmin-mont-ruby-a-luxembou-luxembourg/id-7900775.html",
  "searchedLocation": "Luxembourg (LU)",
  "transactionType": "sale",
  "propertyType": "Apartment block",
  "propertyTypeId": "26",
  "isProject": true,
  "projectUnitsLeft": 7,
  "price": null,
  "priceCurrency": "EUR",
  "priceType": "sale-range",
  "priceMin": 849000,
  "priceMax": 1249000,
  "isPriceConfidential": false,
  "monthlyCharges": null,
  "pricePerSqm": null,
  "surfaceSqm": 68.19,
  "surfaceMinSqm": 68.19,
  "surfaceMaxSqm": 93.57,
  "landAres": null,
  "landSqm": null,
  "bedrooms": null,
  "bedroomsMin": 1,
  "bedroomsMax": 2,
  "bathrooms": null,
  "rooms": null,
  "floor": null,
  "garages": null,
  "hasBalcony": false,
  "hasTerrace": false,
  "hasGarden": false,
  "energyClass": null,
  "thermalInsulationClass": null,
  "address": "Rue de l'Ouest",
  "city": "Luxembourg-Belair",
  "postalCode": "2273",
  "country": "lu",
  "latitude": 49.6149591,
  "longitude": 6.0950029,
  "availability": null,
  "isSold": false,
  "isUnderAgreement": false,
  "advertiserType": "agency",
  "agencyName": "THOMAS & PIRON (Luxembourg)",
  "imagesCount": 14,
  "mainImageUrl": "https://i1.static.athome.eu/images/annonces2/image_/76/e8/87/…_600x315.jpg",
  "scrapedAt": "2026-09-05T09:08:56.457Z"
}
```

Measured across 60 live listings on 2026-09-05: id, description, url, type, city, coordinates, agency and main image on **60 of 60**; a surface on **59**; a single price on **55** (the other 5 are project ranges); energy class on **55**; price per m² on **54**; a postcode on **49**; bathrooms on **41**.

#### Rows that are never charged

| `status` | when |
|---|---|
| `no-results` | atHome reports zero listings for this search. That is its answer, not a failure |
| `no-such-search` | atHome does not know the place and silently widened the search to the whole country. **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, its embedded data was missing, or it claimed a total above zero while yielding nothing |

***

### Pricing

Pay per listing returned. Rows that report an empty search, an unknown place, a filter that matched nothing, or a page that could not be read are **not** charged. A listing 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 admin, print, `/search/`, `/recherche/`, `/suchen/`, `/myathome/`, publishing and error pages. This Actor requests only `/en/buy/<type>/<place>` or `/en/rent/<type>/<place>` with `?page=N`.
- Only public pages are read. No login, no API key.

# Actor input Schema

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

Paste an English atHome listing page, for example https://www.athome.lu/en/buy/apartment/luxembourg or https://www.athome.lu/en/rent/house/esch-sur-alzette, and the three fields below are ignored. Query parameters are dropped; every filter runs here on the rows already fetched.

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

sale = buy, rent = rent.

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

atHome's own words. Other words are not accepted, because atHome would silently answer with houses from the whole country.

## `location` (type: `string`):

A town as atHome spells it: luxembourg, esch-sur-alzette, differdange. Every row carries the place atHome resolved it to (kirchberg, for example, is a village in Germany on atHome). A place atHome does not know is reported as a no-such-search row, not as an empty result - atHome would otherwise silently widen the search to the whole country.

## `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.

## `includeDescription` (type: `boolean`):

The English description (French or German where no English one exists). Turn it off for a smaller dataset - the keyword filter still searches it either way.

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

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

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

Keep only properties whose title, description, type, address, city, energy class 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 atHome 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. Charges are a separate column and are not included.

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

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

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

-1 turns the filter off. A property without a bedroom count is kept.

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

-1 turns the filter off.

## `minSurfaceSqm` (type: `integer`):

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

## `maxSurfaceSqm` (type: `integer`):

0 means no maximum.

## `propertyTypes` (type: `array`):

Partial match on atHome's words: Apartment, Penthouse, Duplex, Studio, House, Semi-detached house, Apartment block …

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

Keep only these 4-digit Luxembourg postcodes.

## `cities` (type: `array`):

Partial match on atHome's city name (Luxembourg-Kirchberg, Esch-sur-Alzette …). Useful with a country-wide search.

## `energyClasses` (type: `array`):

Keep only these Luxembourg energy classes (A, B, C … I). A property without a published class is kept.

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

Drop the grouped project listings that carry a min-max price and several units.

## `excludeSold` (type: `boolean`):

Drop listings atHome flags as sold or under a sales agreement.

## `advertiserType` (type: `string`):

Agency listings, or listings from private sellers. Private sellers are never named.

## `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": "apartment",
  "location": "luxembourg",
  "maxListings": 20,
  "maxPages": 0,
  "includeDescription": true,
  "useProxy": false,
  "keywords": [],
  "keywordMatch": "any",
  "excludeKeywords": [],
  "agencyNames": [],
  "minPrice": 0,
  "maxPrice": 0,
  "requirePrice": false,
  "minBedrooms": -1,
  "maxBedrooms": -1,
  "minSurfaceSqm": 0,
  "maxSurfaceSqm": 0,
  "propertyTypes": [],
  "postalCodes": [],
  "cities": [],
  "energyClasses": [],
  "excludeProjects": false,
  "excludeSold": false,
  "advertiserType": "any",
  "monitoringMode": false,
  "resetMonitoringState": false
}
```

# Actor output Schema

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

One row per property: title and description, 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), confidential prices kept as null, monthly charges in their own column, price per m2, surface, bedrooms, bathrooms, rooms, floor, garages, balcony / terrace / garden flags, energy and insulation class, address with city, postcode and coordinates, availability, sold and under-agreement flags, whether the advertiser is an agency or a private seller (never named), the agency name, photo count and main image. Every row also names the place atHome actually searched. Searches with no results, places atHome does not know, filters that matched nothing, and pages that could not be read come back as their own rows and are not charged. Agent e-mails and phone numbers are not collected.

# 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/athome-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/athome-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/athome-listings --silent --output-dataset

```

## MCP server setup

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