Marktplaats.nl Scraper - Listings, Prices, Sellers & Reviews
Pricing
from $1.00 / 1,000 listing scrapeds
Marktplaats.nl Scraper - Listings, Prices, Sellers & Reviews
From $1/1K. Scrape Marktplaats.nl listings by keyword or URL. Extract title, price, condition, location and coordinates, photos, seller details, ratings and reviews, shipping, views, and category-specific attributes for cars, bikes, electronics, and more.
Pricing
from $1.00 / 1,000 listing scrapeds
Rating
0.0
(0)
Developer
Abot API
Maintained by CommunityActor stats
1
Bookmarked
2
Total users
0
Monthly active users
10 days ago
Last modified
Categories
Features
Share
Marktplaats.nl Scraper: Listings, Prices, Sellers & Automotive Data
Marktplaats.nl Scraper turns Marktplaats, the biggest classifieds and marketplace site in the Netherlands, into clean structured data. Search by keyword and filters or paste listing and result-page URLs, and get 150+ fields per advert: title, price, condition, full description, every photo, location with GPS coordinates, seller name/type/rating/reviews, shipping options, view and favorite counts, and the complete per-category attribute set (cars, bikes, electronics and more). Turn on Incremental mode for a recurring search that returns only what changed, then export to JSON, CSV or Excel, or pull the results straight into your app through the API.
Why This Scraper?
- Two ways to reach listings. Build a search from keywords plus filters, or paste any listing or result-page URL directly.
- Deep per-category attributes. Automotive listings carry make, model, trim, construction year, mileage, fuel, transmission, body type, energy label, options and more, alongside the 150+ fields every listing gets.
- Seller insight. Seller name, private vs professional, active-since, rating, number of reviews and payment method.
- Location-aware filtering. Centre a search on a Dutch postcode with a radius, or narrow by price range and seller type, all applied to the result pages the search reads anyway, with no extra requests.
- Optional detail enrichment. Switch on full detail pages for view/favorite counts, shipping options, bids, seller reviews and the complete description.
- Duplicate-safe pagination. Every result page is deduplicated by listing id, so paging never returns the same card twice.
- Built for monitoring. Resume continues one interrupted run, and Incremental mode returns only new, updated and reappeared listings on a recurring schedule.
Use Cases
- Resellers and flippers: watch a keyword or category for new listings to spot underpriced items quickly.
- Pricing and market research: build a price-history dataset for a product category (bikes, phones, cars) across the Netherlands.
- Automotive marketplaces and lead generation: collect structured vehicle listings (make, model, mileage, fuel, transmission) for aggregation or outreach.
- Local and postcode-based apps: pull listings within a radius of a postcode to power a local classifieds feed.
- Brand monitoring: watch for unauthorized resale of a brand's products across the marketplace.
- Professional-seller discovery: filter to Commercial sellers in a category to build a list of active dealers or shops.
Data You Get
Sample shape: values are illustrative placeholders, not from a live listing.
| Field | Example |
|---|---|
id / itemId | "m0000000000" |
title | "Elektrische fiets Cortina E-U4" |
url | Marktplaats link to the listing |
priceNumeric | 399.0 |
priceType | "FIXED" (also "MIN_BID", "NOTK", "ON_REQUEST") |
currency | "EUR" |
condition | "Zo goed als nieuw" |
brand | "Cortina" |
description | "Nette fiets, weinig gebruikt, incl. oplader." |
category | { "id": 447, "name": "Fietsen", "parentName": "Fietsen en Brommers" } |
location_cityName | "Utrecht" |
location_latitude / location_longitude | 52.0907 / 5.1214 |
sellerName | "FietsenhuisXYZ" |
sellerType | "CONSUMER" (also "COMMERCIAL"), needs Fetch full detail pages |
sellerRating / sellerReviews | 4.8 / 21, needs Fetch full detail pages |
viewCount / savedCount | 58 / 4, needs Fetch full detail pages |
imageUrl / image_urls | listing photo URLs |
attributes | flattened per-category attributes, e.g. { "condition": "Zo goed als nieuw", "delivery": "Ophalen of Verzenden" } |
shipping | { "method": "DELIVERY", "carrierName": "PostNL", "price": "5,49" }, needs Fetch full detail pages |
isBuyersProtectionAllowed | true when the seller allows buyer protection (Fetch full detail pages; null when the page does not say) |
seller.sellerWebsite / seller.sellerWebsiteDisplayUrl | link and display text of a business seller's own website, when one is shown (Fetch full detail pages) |
date | "Vandaag" |
scrapedAt | "2026-01-01T00:00:00+00:00" |
Vehicle listings additionally carry mileage, fuelType, transmission, bodyType, energyLabel, numberOfDoors, numberOfSeats, horsePower, upholstery, licensePlate, apkUntil and constructionYear, pulled from the per-category attribute set and enriched further from the detail page when Fetch full detail pages is on.
Incremental mode adds four more fields to every returned record, described in prose rather than a table because they only appear in that mode:
changeType: one ofNEW,UPDATED,UNCHANGED,REAPPEAREDorEXPIRED.changedFields: the top-level fields that changed since last seen; only non-empty forUPDATED.firstSeenAt: when this listing was first observed by this monitoring campaign.lastSeenAt: when this listing was last observed.
How to Use
- Pick a mode:
search(keywords + filters) orurl(paste listing or result-page links). - Fill in the fields for that mode: keywords, category, price range and postcode for Search, or the URLs to scrape for URL mode. Seller type and the sort order apply either way.
- Turn on Fetch full detail pages if you need view counts, shipping, bids, seller reviews or the full description, and set Max listings to control run size and cost. Max records is an alias of Max listings (the input name other Marktplaats scrapers use); when set it replaces Max listings.
- Click Start, then download the dataset as JSON, CSV or Excel, or read it through the API.
Keyword search with detail enrichment:
{"mode": "search","queries": ["fiets"],"fetchDetails": true,"maxListings": 50}
Browse a category with price and seller filters:
{"mode": "search","queries": [],"categoryId": 91,"minPrice": 5000,"maxPrice": 20000,"sellerType": "COMMERCIAL","maxListings": 100}
Search near a postcode:
{"mode": "search","queries": ["bankstel"],"postcode": "1011AB","distanceMeters": 15000,"maxListings": 40}
URL mode (listing, result and seller pages):
Supported addresses: keyword search (/q/fiets/), a later result page (/q/fiets/p/2/ starts the walk at page 2), a search with sortBy, sortOrder, priceFrom and priceTo in the query string, a search with the filters the site itself puts after the # in the address bar (PriceCentsFrom, PriceCentsTo, sortBy, sortOrder; the # form wins when both are present), a category (/l/<category>/), a subcategory (/l/<category>/<subcategory>/), a category with keyword (/l/<category>/q/fiets/), a listing (/v/.../m<id>-<title>), the short share link of a listing (link.marktplaats.nl/m<id>) and a seller profile (/u/<name>/<sellerId>/; only the number is used, so a wrong name still works). Sort and price taken from a pasted address win over the Result ordering inputs; the Search-mode price, postcode and distance inputs are not used in URL mode, while Seller type still applies. Mobile m.marktplaats.nl and numeric /vip/ or /a/ addresses are skipped with a clear log message.
Behaviour change: the URLs field now ships with ten example addresses as its default and prefill. Starting a URL-mode run without entering any URL scrapes those examples instead of failing, so those example pages are returned and billed like any other listings; replace the list with your own addresses before you start.
Behaviour change for pasted result-page addresses: a pasted /q/ or /l/ address now also applies its own price bounds, sort order, subcategory and /p/N/ start page (earlier builds walked the whole keyword or category from page 1 with the input sort). Existing Incremental tasks that saved such an address keep working: on the first complete run with emitExpired on, tracked listings that fall outside the narrower scope are dropped from tracking without being returned or billed as EXPIRED. A walk that starts on a later page (/p/N/, N above 1) never produces EXPIRED rows, because it does not scan the earlier pages.
URL mode example:
{"mode": "url","urls": ["https://www.marktplaats.nl/l/auto-s/","https://www.marktplaats.nl/v/fietsen-en-brommers/fietsen-dames-damesfietsen/m0000000000-elektrische-fiets"],"fetchDetails": true,"maxListings": 60}
Run it from your code
Python:
from apify_client import ApifyClientclient = ApifyClient("<YOUR_APIFY_TOKEN>")run = client.actor("abotapi/marktplaats-nl-scraper").call(run_input={"mode": "search", "queries": ["fiets"], "maxListings": 20})for listing in client.dataset(run["defaultDatasetId"]).iterate_items():print(listing["title"], listing["priceNumeric"])
JavaScript:
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });const run = await client.actor('abotapi/marktplaats-nl-scraper').call({ mode: 'search', queries: ['fiets'], maxListings: 20 });const { items } = await client.dataset(run.defaultDatasetId).listItems();
Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the Integrations tab.
Caps and cost control
Max listings is the run's real cap: the total number of listings returned across every keyword, category or URL, split proportionally between them. Max pages per search only bounds how many result pages are walked per keyword/category/URL; leave it empty to walk every result page and let Max listings do the capping.
Resume and recurring updates
Two different things, pick the one that matches what you are doing:
- Resume (
resumeFromRunId) continues one specific interrupted or previous large crawl: paste a run id or dataset id and this run skips listings already collected there, returning only the remaining new listings. An automatic same-run checkpoint also protects against platform migrations and the Resurrect button without any input needed. - Incremental mode (
incrementalMode) is for a schedule, for example daily: the actor remembers the previous run of the same search by itself, so you never paste a run id. The first run returns everything asNEW. Later runs return onlyNEW,UPDATEDandREAPPEAREDlistings by default; unchanged listings are suppressed and not charged unlessemitUnchangedis on.EXPIREDrows are only produced after a run that scanned the whole tracked search end to end, uncapped and not resumed; turn onemitExpiredto get them.
Known limitation: combining several overlapping keywords, categories or URLs in one run can stop a later one early. Duplicate protection is tracked per run, not per keyword: once a listing id has been returned by one search in the run, every later search in that same run treats it as already seen. If two of your searches genuinely overlap (for example "fiets" and "fietsen"), a later search can hit a results page made up entirely of listings the earlier search already claimed, and the actor reads that as reaching the natural end of the results, stopping before it reaches further pages that hold listings neither search has seen yet. The visible effect is fewer listings than expected from the second search, and, with Incremental mode and emitExpired on, listings that are genuinely still live can be marked EXPIRED because that search's scan looked complete but was not. Workaround: run each keyword, category or URL you need an exhaustive or Incremental-mode scan of in its own actor run instead of combining several overlapping ones together; unrelated (non-overlapping) keywords in one run are not affected.
The same rule affects Resume when the results have shifted. Resume and the automatic restart checkpoint work by skipping every listing id the earlier run already collected. If new listings were posted at the top of the results since then (for example, one new ad pushes every result down by one), the first results page holds a few fresh listings and the next page consists entirely of listings the earlier run already has, and the actor stops there. The resumed run then returns only those few newest listings and never reaches the part the earlier run had not collected. Workaround: resume soon after the interruption; if a resumed run returns far fewer listings than you expected, run the same search again without Resume, or use Incremental mode for anything you collect on a recurring basis (it is not affected).
Pasted result-page URLs keep the keyword, the category or subcategory, the page number, and sort and price, both from the query-string form (sortBy, sortOrder, priceFrom, priceTo) and from the site's own #PriceCentsFrom:...|PriceCentsTo:...|sortBy:...|sortOrder:... form. Other filters you set on the site before copying the link (distance, postcode, seller type, brand and so on) are not carried over and the run logs which ones were skipped, so the run walks the broader search. For a postcode radius, use Search mode with postcode and distanceMeters.
A run can stop early and still keep what it found. The run watches the platform time limit and stops fetching shortly before it, and it also stops after several requests in a row fail. In both cases the listings already collected are saved and the log says Run stopped early (deadline) or Run stopped early (breaker); run again to continue.
Send results into your apps (MCP connectors)
Optionally pipe the scraped listings into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step after the scrape; the Apify dataset is never changed.
What gets written to the connector: a condensed, human-readable summary of each listing, not the full JSON. Each item becomes one entry with a title and its key fields flattened to plain text. The complete record always stays in the Apify dataset.
- Authorize a connector once under Apify > Settings > Integrations (Notion, Linear, Airtable, or Apify).
- Select it in the "Pipe results into your apps" input field. If the picker is empty, you have not authorized a connector yet.
- For Notion, also set
notionParentPageUrlto the page where listings should be created. - Use
maxNotifyListingsto cap how many listings are written to each connector per run.
The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave the field empty to skip.
Input Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
mode | string | search | search (keywords + filters) or url (paste URLs). |
queries | array | [] (prefilled ["fiets"]) | Search keywords (search mode). Leave empty to browse a whole category. |
categoryId | integer | (none) | Numeric Marktplaats category id to restrict results, for example 91 for Auto's or 447 for Fietsen. |
minPrice | integer | (none) | Keep listings priced at or above this amount, in euros. |
maxPrice | integer | (none) | Keep listings priced at or below this amount, in euros. |
postcode | string | (none) | Dutch postcode to centre a distance search on, for example 1011AB. |
distanceMeters | integer | (none) | Radius in meters around the postcode to search within. |
maxRecords | integer | (none) | Alias of maxListings (the name other Marktplaats scrapers use). When set it replaces Max listings. 0 = the maximum; a run never returns more than 100000 listings. |
urls | array | ten example addresses | Listing or result-page URLs (URL mode). |
sellerType | string | any | any, CONSUMER (private sellers) or COMMERCIAL (professional sellers). Applies to a keyword search and to pasted URLs. |
sortBy | string | OPTIMIZED | OPTIMIZED, SORT_INDEX, PRICE, or DATE. Applies to a keyword search and a pasted result-page URL. |
sortOrder | string | DECREASING | DECREASING (highest/newest first) or INCREASING. |
fetchDetails | boolean | true | Visit each listing page for view/favorite counts, shipping, bids, seller reviews/rating and the full description. |
maxPages | integer | (none) | Optional bound on result pages walked per keyword/category/URL. Leave empty to walk every result page; Max listings does the real capping. |
maxListings | integer | 20 | The run cap: total listings to return across all searches. Set 0 for the maximum; a run never returns more than 100000 listings. |
resumeFromRunId | string | (none) | ID of a previous run of this actor, or a dataset ID. Listings already collected there are skipped, so this run returns only the remaining new listings. |
incrementalMode | boolean | false | Turn on for daily or recurring monitoring of the same search. Later runs return only new, updated and reappeared listings. |
stateKey | string | (none) | Name or share an incremental-mode monitoring campaign. Leave empty to derive a key automatically from the search and filters. |
emitUnchanged | boolean | false | Incremental mode only. Also return, and bill, listings unchanged since the last run. |
emitExpired | boolean | false | Incremental mode only. Also return, and bill, listings no longer found. Only produced after a complete, uncapped, non-resumed scan. |
ignoreFieldsForChanges | array | [] | Incremental mode only. Field names (or dotted paths such as stats.since) that do not count as a change, so a listing where only those fields moved stays UNCHANGED and is not returned or billed again. The fields stay in the output. These are always ignored: scrapedAt, stats, viewCount, savedCount, shipping, shippingOptions and the raw seller_information copy (the shipping carrier list changes between fetches of the same unchanged listing, so it never counts as a change). The structured seller object does count as a change; add seller or a dotted path such as seller.activeSince to ignore it. |
proxy | object | Apify Proxy | Connection settings used for all requests. The default connection works out of the box. |
residentialCountries | array | ["NL", "BE", "DE", "FR", "GB"] | Countries rotated through when the premium connection is used as a fallback. |
maxResidentialRequests | integer | 200 | Caps how many requests may use the premium connection in one run. Set 0 to disable it entirely. |
backupProxyUrl | string | (none) | Optional last-resort proxy gateway URL. Leave blank to use the built-in default. |
mcpConnectors | array | (none) | Optional MCP connectors to pipe a copy of each listing into (Notion, Linear, Airtable, Apify). |
notionParentPageUrl | string | (none) | Notion connector only: page under which listing pages are created. |
maxNotifyListings | integer | 50 | Cap on items written to each connector per run. Does not affect the dataset. |
Output Example
Sample shape: values are illustrative placeholders, not from a live listing.
{"id": "m0000000000","itemId": "m0000000000","title": "Elektrische fiets Cortina E-U4","url": "https://www.marktplaats.nl/v/fietsen-en-brommers/fietsen-dames-damesfietsen/m0000000000-elektrische-fiets","priceNumeric": 399.0,"priceType": "FIXED","currency": "EUR","condition": "Zo goed als nieuw","brand": "Cortina","description": "Nette fiets, weinig gebruikt, inclusief oplader.","categoryId": 447,"category": { "id": 447, "name": "Fietsen", "parentName": "Fietsen en Brommers" },"location_cityName": "Utrecht","location_latitude": 52.0907,"location_longitude": 5.1214,"imageUrl": "https://images.marktplaats.com/api/v1/listing/images/0000.jpg","image_urls": ["https://images.marktplaats.com/api/v1/listing/images/0000.jpg"],"attributes": { "condition": "Zo goed als nieuw", "brand": "Cortina", "delivery": "Ophalen of Verzenden" },"seller": {"id": 0,"name": "FietsenhuisXYZ","type": "CONSUMER","activeSince": "1 jaar","sellerWebsite": null,"sellerWebsiteDisplayUrl": null,"paymentMethod": "ideal"},"isBuyersProtectionAllowed": true,"sellerName": "FietsenhuisXYZ","sellerType": "CONSUMER","sellerRating": 4.8,"sellerReviews": 21,"viewCount": 58,"savedCount": 4,"shipping": { "method": "DELIVERY", "carrierName": "PostNL", "price": "5,49" },"date": "Vandaag","scrapedAt": "2026-01-01T00:00:00+00:00"}
Dataset views
The dataset has four views: Listings Overview (the default), Sellers & Reviews, Listing Detail and Changes (incremental mode). The output schema declares every field, so each column type is fixed and nullable. Fields a listing does not have come back empty.
Plan Requirement
In our measurements the default Apify datacenter connection returned normal listing, category, seller and detail pages, with no challenge page, 403 or 429. That was a single sample of one exit, not a guarantee for every exit, so the actor rotates the connection automatically when a request is refused. If the site refuses every request, the run fails with Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search. Please try again later. On a free Apify plan, which has no proxy access, the message instead says the plan does not include proxy access or that residential proxies are unavailable, and points to https://apify.com/pricing The premium connection fallback is optional headroom for large or frequent runs; Premium connection request budget (maxResidentialRequests) caps it and 0 turns it off.
FAQ
How much does it cost?
The run is billed on three events: the actor start, one dataset item per listing pushed to the dataset, and one detail enrichment per listing that was enriched when Fetch full detail pages is on (it is on by default). Unchanged listings in Incremental mode are neither returned nor charged unless emitUnchanged is on; EXPIRED rows (emitExpired) are returned and charged as dataset items; suppressed, duplicate or undelivered listings are never charged. The Pricing tab shows the current rates. Use Max listings to cap the cost of any run.
Is it legal to scrape Marktplaats.nl?
This actor collects only publicly available listing data. You are responsible for how you use it: follow Marktplaats's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Listing text, prices and attributes are generally facts, but photos may be subject to third-party rights.
What's the difference between Resume and Incremental mode?
Resume (resumeFromRunId) continues one specific interrupted or previous large run by pasting its run or dataset id. Incremental mode (incrementalMode) is for scheduling the same search again and again, for example daily, and getting back only what changed, without ever pasting an id. Resume is best used soon after the interruption: if even a few new listings were posted in between, a resumed run can stop after returning only those newest ones (see "Known limitation" under How to Use).
Can I run several keywords or URLs together and still get everything?
Usually yes, but if two of them genuinely overlap in the underlying catalogue (for example "fiets" and "fietsen"), a later one in the same run can stop early once it hits a results page made up entirely of listings an earlier search in that run already returned. See "Known limitation" under How to Use for the concrete mechanics and the workaround: give each keyword, category or URL you need a complete scan of its own run when the terms are likely to overlap.
Why did my run fail instead of returning an empty dataset?
If the site refuses every request, the run stops with a clear message so "no listings matched" is never confused with "nothing could be read". Run it again in a few minutes. If only part of a run could be read, you still get what was found.
Can I use it with AI agents or MCP?
Yes. Call it from any Apify integration or MCP client, and use the connector field to push results into Notion, Linear or Airtable.
๐ Want more classifieds data?
Pair this actor with these related scrapers from the same team:
| ๐ AutoTrader UK Scraper Pull structured vehicle listings from autotrader.co.uk at scale. Search by filters or use... | ๐ท๏ธ 2dehands & 2ememain Scrape classifieds from 2dehands.be and 2ememain.be by keyword, filters, or URLs. Returns... |
| ๐ mobile.de Scraper Scrape mobile.de vehicle listings with 30+ fields including price, mileage, registration... | ๐ Craigslist Scraper Scrape Craigslist postings in any of 700+ cities worldwide: price, title, posting text... |
| ๐ AutoScout24 Scraper A lean, production-grade scraper for autoscout24.com. Point it at a search, get back... | ๐ท๏ธ OLX Scraper Fast OLX scraper across 9 countries, including Poland, Romania, Portugal, Ukraine... |
๐ Browse all abotapi scrapers
๐ฌ Support
- ๐ Found a bug or a missing field? Open a ticket on the Issues tab. We usually reply within hours.
- ๐ ๏ธ Need another site, extra fields or a private build? Email contact@abotapi.com.
- โญ Enjoying it? A quick review on the actor page helps other users find it.