# Funda Properties Scraper (`scrapyx/funda-properties-scraper`) Actor

Scrapes Dutch property listings from Funda: asking price, price per m², living area, rooms, build year, energy label, VvE contribution, address with postcode and neighbourhood, photos and the full Dutch feature table. For sale and for rent.

- **URL**: https://apify.com/scrapyx/funda-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 $1.26 / 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/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

## Funda Properties Scraper

Dutch properties **for sale and to rent** from Funda — full address with
postcode, price, floor area, rooms and bedrooms, energy label, the selling
agent, photos and listing dates, plus a **23-group facet breakdown of the
whole market** on every search.

Search by plain place name; areas are resolved *and verified* for you. HTTP
only, no browser, no login.

### Input

```json
{
  "channel": "buy",
  "locations": ["Amsterdam", "Den Haag"],
  "minPrice": 300000,
  "maxPrice": 750000,
  "minBedrooms": 2,
  "maxPages": 5
}
```

`locations` also accepts a Funda area slug (`provincie-noord-holland`,
`regio-groot-amsterdam`, `amsterdam/uilenburg`, `1011lm`) or a full Funda
search URL, so you can paste whatever you already have.

### What you get

| `recordType` | One per | Carries |
| --- | --- | --- |
| `SEARCH_SUMMARY` | location | resolved slug, area name and type, its centroid and parent areas, the slug forms tried, the upstream match count, the reachable ceiling, pages fetched, duplicates dropped, and the **full facet breakdown** |
| `LISTING` | property | id, URL, full address with postcode and neighbourhood, price, floor area, rooms, energy label, agent, photos, listing date — plus the raw upstream object |
| `ERROR` | failed input | why |

```jsonc
{
  "recordType": "LISTING",
  "listingId": 8138160,
  "listingUrl": "https://www.funda.nl/detail/koop/amsterdam/appartement-nieuwe-uilenburgerstraat-9-g/44583523/",
  "addressLine": "Nieuwe Uilenburgerstraat 9 G",
  "postalCode": "1011LM",
  "city": "Amsterdam",
  "neighbourhood": "Uilenburg",
  "district": "Nieuwmarkt/Lastage",
  "province": "Noord-Holland",
  "objectType": "apartment",
  "price": 975000, "currency": "EUR", "priceCondition": "kosten_koper",
  "floorArea": 118, "numberOfRooms": 3, "numberOfBedrooms": 2,
  "energyLabel": "C",
  "agentName": "House of MAY Real Estate",
  "publishDate": "2026-09-06T09:00:01+02:00"
}
```

### Optional: the detail pass

Set `fetchDetails: true` to fetch each listing's own page as well. It adds:

- **`latitude` / `longitude`** — search results carry **no coordinates at
  all**, so this is the only way to map Funda properties
- **`description`** — the full Dutch listing text
- **`pricePerSqm`**, **`vveContributionMonthly`**, **`buildYear`**,
  `volumeM3`, `insulation`, `heating`, `situation`, `gardenOrientation`,
  `constructionKind`, room and bathroom descriptions
- **`featureGroups`** — the complete nine-group Dutch feature table
  (Overdracht, Bouw, Oppervlakten en inhoud, Indeling, Energie, Kadastrale
  gegevens, Buitenruimte, Parkeergelegenheid, VvE checklist)
- `brochureUrl`, `canonicalUrl`, `detailLabels`, `isSoldOrRented`

It is **off by default because it costs roughly 15× the requests** — a search
page returns 15 listings, a detail page returns one. A detail page that fails
does not lose its row: the search data stays and `detailError` records why.

### Known limits — read these before you rely on the output

**Funda's default search hides more than it shows.** Amsterdam reports
**6,194** matches — but that is only what is on the market *now*
(4,068 available + 2,126 under negotiation). A further **13,698 sold or
rented** listings sit behind the `availability` filter, which the site does
not apply by default. If you are measuring a market rather than shopping,
select all three availability states. Every summary row reports the
availability facet counts, so you can always see what was excluded.

**Past 10,000 matches, Funda re-serves its last page forever.** It does not
return an empty page and it does not error — page 668, page 6,148 and page
20,000 of a nationwide search are byte-identical. A "keep paging until the
page comes back empty" consumer would loop indefinitely on the same 15 rows.
This actor computes the last page from the count instead, and every summary
carries `reachableCeiling`. **To get past it, narrow the search** — a price
band, an object type, or a smaller area. Below 10,000 the count is exact and
fully reachable: Amsterdam's 6,194 comes back as 412 full pages plus a final
page of 14.

**Place names are case-sensitive upstream.** `amsterdam` returns 6,194
listings; `Amsterdam` returns **zero**, with a normal 200. This actor
normalises names before use, so you can type them naturally — but it is why
an area that does not resolve produces an `ERROR` row rather than a guess:
**omitting the area entirely returns all 93,867 listings in the Netherlands**,
so a silent fallback would hand you the wrong country-sized answer. Funda's
slug is also not always the obvious one — 's-Hertogenbosch is `den-bosch`.
Pass a slug from a Funda search URL when you need certainty.

**New-build project rows carry a price *range*, not a price.** 62 of
Amsterdam's rows are `listingType: "group"` — one row standing for a whole
development, with `selling_price` of `[425000, 769000]` and no room counts.
Those rows have `price: null`, `priceMin`/`priceMax` set, and `isProject:
true`. Filter them out with `listingTypes: ["single"]` if you want homes only.

**Some homes are listed for sale *and* for rent at once.** Funda gives these
a `/detail/koophuur/` URL and puts both numbers in one object — 14 of 30 rows
on a live Amsterdam rent search. `price` always follows the channel you
queried, and `salePrice`, `rentPrice` and `isDualListed` are all populated so
you never have to guess which number you got.

**A price of `null` can be correct.** `priceType: "on_request"` is Funda's
"prijs op aanvraag" — a real listing whose price is genuinely not published.

**Two plausible sort options do not exist.** Funda silently ignores an
unknown `sort` and falls back to relevancy with a normal-looking page —
including `floor_area_up` and `plot_area_up`, which are exactly what you would
guess for "smallest first". Only orderings verified to actually change the
result order are offered, and anything else is refused up front.

**Bad filter values fail two different ways**, so both are refused before a
request is made: an unknown property type or energy label returns an empty
result set (reads as "nothing matched"), while an unparseable price filter is
*silently ignored* and returns the **unfiltered** set (reads as a successful
search).

### Technical

Server-rendered Nuxt 3. The whole result set is strict JSON inside
`<script id="__NUXT_DATA__">`, but in Nuxt's `devalue` flat-array form — a
list where every value is either a literal or an index into that same list,
with real cycles — so it is resolved rather than subscripted. 15 listings per
page, paged with `&search_result=`.

Area resolution rides on the search request itself: Funda's response reports
which area it actually applied, so a name is resolved by trying its slug forms
(bare, `gemeente-`, `provincie-`, `regio-`) and reading back what upstream
used. When the first candidate resolves — the common case — that same response
*is* page 1, so resolution costs no extra request.

All seven TLS fingerprint profiles tried returned 200, so the pool is a real
rotation rather than an allowlist.

`robots.txt` was checked at **path** level on the exact URLs fetched.

Full recon trail is in `CRAWLING_METHOD.md`.

# Actor input Schema

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

Properties for sale (koop) or to rent (huur). Rent rows carry a monthly price condition that sale rows do not - comparing a rent amount to a sale amount without reading it compares a month of rent to a purchase price.

## `locations` (type: `array`):

Where to search. Accepts a plain place name (Amsterdam, Den Haag, Noord-Holland), a Funda area slug (provincie-noord-holland, regio-groot-amsterdam, amsterdam/uilenburg, 1011lm), or a Funda search URL. Names are normalised and then verified against Funda's own resolution, and an area that does not resolve produces an ERROR row rather than a search - because Funda answers a MISSING area by returning every listing in the Netherlands. Note Funda's slug is not always the obvious one: 's-Hertogenbosch is 'den-bosch'.

## `sortBy` (type: `string`):

Result ordering. Only orderings verified to actually change the result order are offered: Funda silently ignores an unknown sort and falls back to relevancy with a normal-looking page, and two plausible values ('smallest floor area first', 'smallest plot first') do exactly that.

## `availability` (type: `array`):

Which market states to include. LEAVING THIS EMPTY IS NOT 'everything': Funda's default shows only what is on the market now. For Amsterdam that is 6,194 listings, while a further 13,698 sold or rented ones sit behind this filter. Select all three to measure the whole market including sold stock. Every summary row reports the availability facet counts so you can see what was excluded.

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

Lower price bound. For rent this is per month. Verified to genuinely narrow results (Amsterdam 6,194 -> 3,849 at 500,000).

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

Upper price bound. For rent this is per month.

## `objectTypes` (type: `array`):

Optional filter. Verified: Amsterdam has 5,282 apartments against 775 houses. An unrecognised value is refused up front, because Funda answers one with an empty result set rather than an error - which would read as 'nothing matched'.

## `constructionTypes` (type: `array`):

Resale or new-build. Note new-build searches surface project rows, which stand for a whole development rather than one home - those carry a price RANGE and are flagged with isProject.

## `energyLabels` (type: `array`):

Dutch energy labels. 'unknown' is a real value upstream and covers 927 of Amsterdam's listings, so it is offered rather than hidden.

## `exteriorSpaceTypes` (type: `array`):

Balcony, garden or terrace.

## `listingTypes` (type: `array`):

Individual homes or new-build project entries. A project row represents many units at once: its price and floor area are ranges, and its room counts are null. Leave empty for both.

## `minFloorArea` (type: `integer`):

Lower bound on living area. Verified: 100 m2 narrows Amsterdam to 1,895.

## `minRooms` (type: `integer`):

Lower bound on total rooms. Project rows carry no room count and drop out when this is set.

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

Lower bound on bedrooms.

## `publicationDate` (type: `string`):

Only listings published in the last N days. Funda accepts its own windows only - any other number is silently ignored and returns the unfiltered set.

## `fetchDetails` (type: `boolean`):

Adds one request PER LISTING to pull what the search results cannot carry: the full Dutch description, the nine-group feature table (asking price per m2, VvE contribution, build year, insulation, cadastral parcels, VvE checklist), the brochure link - and the GEO COORDINATES, which search results do not contain at all. Off by default because it is roughly 15x the requests: a search page returns 15 listings, a detail page returns one. A detail page that fails does not lose the row; the search data stays and detailError says what happened.

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

Pages to fetch per location; each page is 15 listings. Capped at 666 because Funda's result window is 10,000 - past it Funda re-serves its final page forever with a normal 200 rather than ending, so the run stops on the computed last page instead of waiting for an empty one.

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

Optional cap on total listing rows across the run, shared across every location. Counted after de-duplication.

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

How many requests may be in flight at once. Only matters when several locations are listed; paging within one is sequential.

## `minRequestInterval` (type: `number`):

The honest speed control: the shortest gap between two request starts. Search pages are about 400 KB each.

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

Apify Proxy on the shared datacenter pool. This is the default because it is included in your plan at no extra cost and this target works through it. If you start seeing blocks, challenges or empty results, switch the group here to Residential -- it uses real consumer IPs and gets through more, but Apify bills residential traffic per gigabyte, so leave it off unless you need it.

## Actor input object example

```json
{
  "channel": "buy",
  "locations": [
    "amsterdam"
  ],
  "sortBy": "relevancy",
  "availability": [],
  "objectTypes": [],
  "constructionTypes": [],
  "energyLabels": [],
  "exteriorSpaceTypes": [],
  "listingTypes": [],
  "publicationDate": "",
  "fetchDetails": false,
  "maxPages": 5,
  "maxConcurrency": 2,
  "minRequestInterval": 1,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# 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/funda-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/funda-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/funda-properties-scraper --silent --output-dataset

```

## MCP server setup

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