By the end of this tutorial you will have a Python script that pulls Dutch homes for sale or for rent from funda.nl into structured rows: asking price, address, living area, rooms, bedrooms, energy label, agency and photo links.
Funda is a Dutch property portal, and its search pages are not friendly to a quick requests script. In my tests on 7 November 2026, plain HTTP clients got an "Access Denied" stub or a verification page instead of results. Funda Scraper (reventadata/funda-scraper) is the Apify Actor I built to deal with that. You send it a search, it returns one row per listing.
This guide runs it from Python with the Apify API client, and the sample rows below come from a real capture of a Utrecht rental search, not from an invented example.
Prerequisites
You need three things:
- Python 3.11 or newer (the Apify client for Python 3.x requires it).
- A free Apify account, which you can create with an email address or a GitHub login.
- Your API token, from Settings > API & Integrations in Apify Console.
Install the client and export the token:
pip install -U "apify-client~=3.0"
export APIFY_API_TOKEN="your_apify_token_here"
No scraping code is needed on your side. The Actor does the fetching.
What you'll achieve
You will search funda.nl for rentals in Utrecht up to 1,500 EUR per month, read the rows in Python and print a one-line summary per listing, including a price per square metre that you work out yourself from two fields.
Funda lists more than homes, so a rental search also returns parking spaces and storage units. You will see how to spot them and how to filter them out.
The same script works for a buyer comparing asking prices across towns, a tenant watching new rentals, or an analyst building a table of energy labels by neighbourhood. Only the input changes.
Step 1: Open Funda Scraper
Open the Funda Scraper page on Apify Store. The Try for free button there opens the Actor in Apify Console. This tutorial uses the API from Python, so you only need the Actor name, reventadata/funda-scraper.
Step 2: Choose the search
The search is described by a handful of input fields:
-
offering:saleorrent. Anything else is refused before the run starts. -
areas: a list of Funda area slugs such asamsterdamorutrecht, copied from theselected_areapart of a funda.nl search URL. Each area is one search, and a listing found twice is kept once. Leave the list empty and the Actor searches the whole Netherlands. -
objectType: empty (every type),houseorapartment. -
sort:relevance(Funda's default) ornewest. -
maxPriceandminPrice: in EUR, and for rentals per month. -
minLivingAreaM2,maxLivingAreaM2,minBedrooms: size filters. -
maxItemsandmaxPages: caps on rows and on pages per area.
A search with a maxPrice sends the price range to Funda, so Funda does that filtering. The living-area and bedroom limits are applied by the Actor after it has read a page, so they remove rows but do not reduce the number of pages fetched.
To search two towns for larger apartments for sale, the input would look like this:
{
"offering": "sale",
"areas": ["amsterdam", "utrecht"],
"objectType": "apartment",
"minLivingAreaM2": 60,
"minBedrooms": 2,
"maxItems": 30,
"maxPages": 3
}
Step 3: Run it from Python
This script starts the Actor, waits for it, reads the dataset and prints two lines per listing:
import os
from apify_client import ApifyClient
# 1. Initialise the client with your token from the environment
client = ApifyClient(os.environ["APIFY_API_TOKEN"])
# 2. Describe the search: rentals in Utrecht up to 1,500 EUR a month
run_input = {
"offering": "rent",
"areas": ["utrecht"],
"maxPrice": 1500,
"maxItems": 15,
"maxPages": 1,
}
# 3. Start the Actor and wait for it to finish
run = client.actor("reventadata/funda-scraper").call(run_input=run_input)
if run is None or run.status != "SUCCEEDED":
status = run.status if run else "unknown"
raise SystemExit(f"Run did not succeed (status: {status}).")
# 4. Read the rows from the run's default dataset
items = client.dataset(run.default_dataset_id).list_items().items
print(f"Retrieved {len(items)} listings.\n")
# 5. Print two lines per listing, with a price per square metre worked out here
for item in items:
street = " ".join(
part for part in (item.get("street"), item.get("houseNumber"), item.get("houseNumberSuffix")) if part
)
price, area = item.get("price"), item.get("livingAreaM2")
per_m2 = f"{price / area:.1f} EUR/m2" if price and area else "no living area"
print(f"{street}, {item.get('city')} | {price} EUR/month | {area} m2 | {per_m2}")
print(f" {item.get('propertyType')}, bedrooms: {item.get('bedrooms')}, energy label: {item.get('energyLabel')}")
Fetching goes through an unblocking service, one page at a time, so keep maxPages small while you test.
Step 4: Read the output
The loop prints one block per listing. This is the start of its output on a capture dated 2026-11-07 (a Utrecht rental search with maxPrice 1,500, 15 rows on page 1):
Koningsweg 135 E, Utrecht | 290 EUR/month | None m2 | no living area
parking, bedrooms: None, energy label: unknown
Hooft Graaflandstraat 2 A9, Utrecht | 1425 EUR/month | 27 m2 | 52.8 EUR/m2
apartment, bedrooms: 1, energy label: A++++
Oranje Vrijbuiterslaan 55, Utrecht | 1132 EUR/month | 48 m2 | 23.6 EUR/m2
apartment, bedrooms: 1, energy label: A
The first listing is a parking space: no living area, no bedrooms. The Actor keeps it because objectType was left empty, and it reports the missing values as null rather than guessing.
Step 5: Add coordinates and the description (optional)
Set includeDetails to true and the Actor opens each kept listing's own page for latitude, longitude, the full Dutch description and the feature table. A listing whose page cannot be read still ships, with those four fields null.
The detail fields look like this on a capture of one rental listing's page (description and features shortened):
{
"latitude": 52.074806,
"longitude": 5.119643,
"description": "Karakteristiek appartement met patio en uitzicht over de Vaartsche Rijn! ...",
"features": {
"Aangeboden sinds": "November 6, 2026",
"Huurprijs": "€ 1.950 per maand (geen servicekosten)",
"Waarborgsom": "€ 3.900 eenmalig",
"Soort bouw": "Bestaande bouw",
"Bouwjaar": "1920",
"Wonen": "85 m²",
"Aantal kamers": "2 kamers (1 slaapkamer)",
"Verwarming": "Gehele vloerverwarming"
}
}
Understanding your results
Every row carries 38 fields. The ones you will use most:
-
price,priceCondition,priceType: the amount, and how Funda labels it (per_monthfor rent,kosten_koperfor sale). -
street,houseNumber,houseNumberSuffix,postalCode,city,district,neighbourhood: the address, split up. -
livingAreaM2,plotAreaM2,rooms,bedrooms,energyLabel: size and energy label, as published. An unlabelled home shows"unknown". -
agencyName,agencyUrl,agencyAssociation,brokerName: the listing agency, and the broker when the listing names one. -
imageUrls,publishedAt,openHouses,promoText: photos, publication time, open-house windows, promo line.
One complete row from the same capture (imageUrls cut to its first entry):
{
"id": "8178650",
"url": "https://www.funda.nl/detail/huur/utrecht/appartement-hooft-graaflandstraat-2-a9/80987659/",
"listingType": "rent",
"price": 1425,
"priceType": "regular",
"priceCondition": "per_month",
"currency": "EUR",
"propertyType": "apartment",
"constructionType": "resale",
"status": "none",
"street": "Hooft Graaflandstraat",
"houseNumber": "2",
"houseNumberSuffix": "A9",
"postalCode": "3525VV",
"city": "Utrecht",
"municipality": "Utrecht",
"province": "Utrecht",
"district": "Wijk 07 Zuid",
"neighbourhood": "Nieuw Hoograven-Zuid",
"livingAreaM2": 27,
"plotAreaM2": null,
"rooms": 2,
"bedrooms": 1,
"energyLabel": "A++++",
"publishedAt": "2026-10-05T15:49:58.8659613+02:00",
"promoText": null,
"imageUrls": [
"https://cloud.funda.nl/tiara-media/64c806c0-d856-4ab7-be06-c84e811130e4/875e471e-0738-4a5c-bcf2-6d1f795314f4"
],
"agencyName": "Otten en Stok Makelaars",
"agencyUrl": "https://www.funda.nl/makelaar/63878-otten-en-stok-makelaars/",
"agencyId": 63878,
"agencyAssociation": "VN",
"brokerName": null,
"openHouses": [],
"searchUrl": "https://www.funda.nl/zoeken/huur?selected_area=%5B%22utrecht%22%5D&price=%220-1500%22&search_result=1",
"latitude": null,
"longitude": null,
"description": null,
"features": null
}
The last four fields are null without details.
Pro tips
-
Filter by type early. In that capture 5 of 15 rows were parking spaces or storage. Set
objectTypetoapartmentorhouseto leave them out. -
Use
newestfor monitoring.sort: "newest"puts fresh listings first, which suits a schedule that checks for new homes. -
minPricealone filters after the fetch. It is only sent to Funda together withmaxPrice, so by itself it removes rows but does not save pages. -
Add
includeDetailsonly when you need it. See Step 5: it costs one extra request per row. -
Compute your own ratios. There is no price-per-square-metre field. Divide
pricebylivingAreaM2yourself, as the script does, and skip rows where either isnull. - Mind the ceiling. Funda serves at most 666 pages of 15 results (9,990) for any one search. If a search is larger, the run logs a warning; narrow it with a price range, a type or a smaller area.
Troubleshooting
- Zero rows, status SUCCEEDED. The site answered but nothing matched, or the area slug is unknown. The status message says so. Check the spelling against a funda.nl URL.
-
Zero rows, status FAILED. A malformed slug or a
maxPricebelowminPriceends the run with a message to fix the input. Pages that could not be read also fail a run that saved nothing. -
Fewer rows than expected. The log ends with
Skipped N: reasonlines for rows removed by your limits, andPushed N row(s).. Read it withclient.log(run.id).get().
Funda's terms of use restrict automated collection, so check that your use of the data complies with them and with data-protection law.
Next steps
For current pricing and details, see the Store page.
To repeat a search, save it as an Apify task and attach a schedule, which runs an Actor or task on a cron expression. Datasets export as JSON, CSV, XML, Excel, HTML table, RSS or JSONL (dataset docs), and a webhook can call your own endpoint when a run finishes (webhook docs).
What are you planning to search for on Funda? Drop a comment! 💬
About the author: reventadata builds and publishes data extraction Actors on the Apify Store.
Top comments (0)