# Funda.nl Scraper — Dutch Property Listings, Photos, 38 Fields (`skubadev/funda-scraper`) Actor

Scrape Funda.nl property listings for sale and rent across the Netherlands — Amsterdam, Rotterdam, Utrecht and every Dutch city. 38 fields per home: price, m2, rooms, energy label, geo, agent office, photo URLs, days on market. A site redesign does not break it.

- **URL**: https://apify.com/skubadev/funda-scraper.md
- **Developed by:** [Anatoly Skuba](https://apify.com/skubadev) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.49 / 1,000 listings

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

## Funda.nl Scraper — Dutch Property Listings, Photos, 38 Fields

Scrape property listings from **Funda.nl**, the largest real estate portal in the
**Netherlands** — houses and apartments, **for sale** and **for rent**, in Amsterdam,
Rotterdam, The Hague, Utrecht, Eindhoven and every other Dutch city.

38 fields per property, including **photo URLs**, price per m², energy label,
full geo hierarchy and days on market. No login, no cookies, no browser needed.

### Empty results, and why you will not get them here

A scraper built on CSS selectors fails quietly. When Funda changes its markup,
or answers an automated request with a block page, you do not get an error — you
get an empty table, or a column of empty values, and you find out later.

This Actor is built the other way round:

- it reads the data **Funda already renders into the page**, so a redesign of
  the visual layout does not change what it returns;
- it goes through **residential proxies** with polite delays and retries every page;
- at the end of every run it prints **how many pages failed and how many rows it
  skipped**, and warns you when a promised field came back mostly empty. You never
  have to guess whether the file is complete;
- you pay **per listing returned**. A run that finds nothing costs you the Actor
  start fee of $0.00005 and nothing else.

### Quick Start

1. Open [funda.nl](https://www.funda.nl), set your filters
2. Copy the address of the **results page** (not a single listing)
3. Paste it into `searchUrls` and press **Start**

The default input works out of the box: homes for sale in Amsterdam.

```json
{
  "searchUrls": ["https://www.funda.nl/zoeken/koop?selected_area=amsterdam"],
  "maxItems": 1000
}
```

### Why this one does not break

Most scrapers read the page with CSS selectors. When the site changes its markup,
they silently return nothing.

This Actor reads the **data the server already renders into the page** — Funda is
built on Nuxt, and the full result set ships inside the page as structured data.
There are no CSS selectors anywhere in the code.

It goes one step further: the listing array is located **by its shape** — the set of
fields every record must have — rather than by a fixed path into the payload. A
framework upgrade that moves the data elsewhere does not break extraction.

Two more consequences:

- **No browser.** Plain HTTP requests, so runs are fast and compute is cheap
- **Typed fields.** Numbers arrive as numbers, dates as ISO timestamps

A failed page is retried three times with backoff and never aborts the run.
A malformed listing is skipped, not the page around it. Every run writes a
`RUN_STATS` record with the real failure rate — you can audit it yourself.

### Output

One row per listing:

| Field | Notes |
|---|---|
| `id`, `url` | Stable listing id and direct link |
| `type` | `apartment`, `house`, … |
| `title` | Street, number and suffix |
| `price`, `currency`, `marketingType` | `BUY` / `RENT` |
| `priceCondition`, `priceType` | e.g. `kosten_koper` |
| **`pricePerSqm`** | Calculated for you |
| `livingSpace`, `floorAreaMin`, `floorAreaMax` | m² |
| `rooms`, `bedrooms` | Numbers, not strings |
| `street`, `houseNumber`, `postcode`, `city` | |
| `quarter`, `district`, `municipality`, `province` | Full Dutch geo hierarchy |
| `preciseAddress` | Whether the address is a verified BAG address |
| **`daysOnMarket`** | Days since publication |
| `publishedAt` | ISO 8601 |
| `energyEfficiencyClass` | A, B, C … |
| `constructionType`, `zoning`, `status` | resale / new build, residential, … |
| `realtorCompany`, `realtorId` | Agency office — see below |
| `country` | Always `NL` |
| `listingKind` | Listing kind as the portal classifies it |
| **`imageUrls`** | Full-size photo URLs, ready to download |
| `imagesCount` | How many photos this listing has |
| `sourceUrl`, `scrapedAt` | |

### The three things that go wrong with property scrapers

These are the complaints you will find under other Funda scrapers in this Store.
Here is what this Actor does about each of them.

**"It stopped working."** Most scrapers read the rendered page with CSS selectors.
The portal ships a new layout and they return nothing — often silently. This Actor
reads the structured data the server already puts inside the page, so a visual
redesign does not break extraction. On top of that a health check runs every
morning, so a real break is found before a customer runs into it.

**"Not fetching all records" / "pagination is not working."** Every page is retried,
and the retry covers parsing, not just the network. The run reports its own failure
rate in `RUN_STATS`: if any page was lost you will see it, instead of quietly getting
a shorter file.

**"Huge amount of billing for unknown reason."** You are charged per listing written
to the dataset, not per request. `maxItems` is a hard stop. Turn on
`skipWithoutPrice` and rows with an empty price are dropped **before** billing.
A container restart cannot cause double charging — run state is persisted.

### No personal data

This Actor does **not** return the estate agent's personal contact — no names,
phone numbers or email addresses — even though they appear in the source page.
Only the agency office name is included, which is business information.

If you need personal contact data, this is not the right Actor, and under GDPR
you would need your own lawful basis for processing it.

### Limits — what this Actor does not do

- Search result pages only. Full listing detail pages are not fetched
- Netherlands (`funda.nl`) only
- No listing photos
- No price history. `daysOnMarket` comes from the listing's own publication date

### Pricing

**$2.49 per 1,000 listings** — $0.00249 per listing — plus $0.00005 per Actor start.
Platform usage (compute and proxy) is included in that price and is not billed
to you separately.

A run that returns nothing costs nothing beyond the Actor start.

#### Cost control

Set **Max listings** before you press Start and the run's cost is fixed in advance:
1,000 listings is $2.49, 250 listings is $0.62. The Actor stops exactly at that
number, on every plan, and writes a `RUN_STATS` record with what it actually did.
Nothing here depends on your plan tier — the behaviour on the Free plan is the
same as on any other, just bounded by the credit the plan gives you.

### Development

```bash
npm test    # regenerates the fixture and runs offline parser tests, no network
```

Tests cover the Nuxt payload hydration, shape-based record lookup, GDPR filtering,
incomplete records, and a deliberately malformed listing.

# Actor input Schema

## `searchUrls` (type: `array`):

Open funda.nl, set your filters, then copy the address of the RESULTS page. It must contain /zoeken/ — a single listing page (/detail/...) will not work.

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

Stop after this many listings across all URLs.

## `maxPagesPerUrl` (type: `integer`):

0 means no limit — follow pagination to the end.

## `politeDelayMs` (type: `integer`):

Keeps request rate polite. Lower it only if you know what you are doing.

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

Dutch residential proxy is recommended.

## `probeUrls` (type: `array`):

Paste any URL here and the Actor only checks whether it is reachable and whether the page carries the structured data this Actor reads — then stops without scraping and without charging per listing. The full result is written to the key-value store under PROBE\_RESULTS. Leave empty for normal runs.

## `skipWithoutPrice` (type: `boolean`):

A few entries arrive with an empty price. Turn this on to drop them — they are filtered out before the dataset is written, so you are not charged for them.

## Actor input object example

```json
{
  "searchUrls": [
    "https://www.funda.nl/zoeken/koop?selected_area=amsterdam"
  ],
  "maxItems": 1000,
  "maxPagesPerUrl": 0,
  "politeDelayMs": 700,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "NL"
  },
  "probeUrls": [],
  "skipWithoutPrice": false
}
```

# Actor output Schema

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

All listings returned by this run, one object per listing.

## `listingsCsv` (type: `string`):

The same listings as a CSV file.

# 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 = {
    "searchUrls": [
        "https://www.funda.nl/zoeken/koop?selected_area=amsterdam"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("skubadev/funda-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 = { "searchUrls": ["https://www.funda.nl/zoeken/koop?selected_area=amsterdam"] }

# Run the Actor and wait for it to finish
run = client.actor("skubadev/funda-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 '{
  "searchUrls": [
    "https://www.funda.nl/zoeken/koop?selected_area=amsterdam"
  ]
}' |
apify call skubadev/funda-scraper --silent --output-dataset

```

## MCP server setup

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