DEV Community

Cover image for How to Scrape Marktplaats Listings in Python with Marktplaats Scraper
ReventaData
ReventaData

Posted on Originally published at apify.com

How to Scrape Marktplaats Listings in Python with Marktplaats Scraper

By the end of this tutorial you will have a Python script that pulls classified ads from marktplaats.nl into structured rows: title, price and price type, city, seller, condition, delivery and photo links. The same Actor also reads the Belgian sites 2dehands.be and 2ememain.be.

Marktplaats is the Dutch second-hand marketplace. Marktplaats Scraper (reventadata/marktplaats-scraper) is the Apify Actor built for it: you send a keyword or a category, and it returns one row per listing.

This guide runs it from Python with the Apify API client. The sample rows below come from a real run of the Actor's example task on 15 November 2026 (keyword iphone, site nl, 10 rows), not from an invented example.


Prerequisites

You need three things:

  1. Python 3.11 or newer (the Apify client for Python 3.x requires it).
  2. A free Apify account, which you can create with an email address or a GitHub login.
  3. 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"
Enter fullscreen mode Exit fullscreen mode

No scraping code is needed on your side. The Actor does the fetching.


What you'll achieve

You will search marktplaats.nl for iphone, read the first rows in Python and print one line per listing: title, price, how the price is meant, whether the seller looks private or business, and the city.

You will also see why some rows have no price at all. Marktplaats has bidding ads and "see description" ads, and the Actor reports those honestly as null instead of a made-up zero.

The same script works for a bargain hunter watching one model, a reseller comparing asking prices, or an analyst counting business sellers in a category. Only the input changes: another keyword, a category path, the Belgian sites, a price ceiling.


Step 1: Open Marktplaats Scraper

Open the Marktplaats 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 all you need is the Actor name, reventadata/marktplaats-scraper.


Step 2: Choose the search

The search is described by a handful of input fields:

  • country: nl (marktplaats.nl), be-nl (2dehands.be, Dutch) or be-fr (2ememain.be, French). Anything else is refused before the run starts.
  • searchQueries: a list of keywords. Each keyword is one search across every category.
  • categoryPaths: a list of category addresses, the part after /l/ in a category URL, such as telecommunicatie/mobiele-telefoons-apple-iphone. A pasted full address works too. At least one of searchQueries and categoryPaths is required.
  • sellers: any, private or business.
  • minPrice and maxPrice: euro bounds, inclusive.
  • excludePromoted, excludeReserved and requireKeywordMatch: switches, all off by default.
  • maxItems and maxPages: caps on rows and on pages per search. A page holds 30 listings.

The price, seller and promotion filters are applied by the Actor after it has read a page, so they remove rows but do not reduce the number of pages read. There is no sort option: rows come in the site's own order.

The two Belgian sites show the same ads in two languages, and category slugs differ between them. To read private sellers in a French category on 2ememain.be, without the paid top-of-the-day placements, the input would look like this:

{
    "country": "be-fr",
    "categoryPaths": ["telecoms/telephonie-mobile-apple-iphone"],
    "sellers": "private",
    "excludePromoted": true,
    "maxItems": 30,
    "maxPages": 1
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Run it from Python

This script starts the Actor, waits for it, reads the dataset and prints one line 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: the keyword "iphone" on marktplaats.nl, first page only
run_input = {
    "country": "nl",
    "searchQueries": ["iphone"],
    "sellers": "any",
    "maxItems": 10,
    "maxPages": 1,
}

# 3. Start the Actor and wait for it to finish
run = client.actor("reventadata/marktplaats-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 one line per listing; price is None when the listing shows no amount
for item in items:
    price = f"{item['price']:.0f} EUR" if item.get("price") is not None else "no amount"
    print(f"{item['title']} | {price} ({item['priceType']}) | {item['sellerTypeInferred']} | {item.get('city')}")
Enter fullscreen mode Exit fullscreen mode

Keep maxPages small while you test: the Actor reads one page after another.


Step 4: Read the output

The loop prints one line per listing. This is the complete output of the run behind this article (keyword iphone on nl, 10 rows). The run finished in about 5 seconds:

iPhone verkopen? Ontvang de hoogste prijs voor je iPhone! | no amount (SEE_DESCRIPTION) | private | Amstelveen
iPhone 15 - Zeer goede staat, 80-84% batterij | no amount (FAST_BID) | private | Franeker
Apple iPhone 14 • Refurbished • 24M Garantie • vanaf €279,- | 279 EUR (FIXED) | business | Haarlem
Iphone 14 pro, 128 GB, onbeschadigd | no amount (FAST_BID) | private | Alphen
Iphone 14 pro max 256 GB | 330 EUR (MIN_BID) | private | Voorburg
iPhone 17 Pro | Black Friday €15 korting + 4 accessoires | 999 EUR (FIXED) | business | None
iPhone 14 plus | 250 EUR (MIN_BID) | private | Gouda
iPhone 13 128gb | 175 EUR (FIXED) | private | Terneuzen
iPhone  17 256GB alle kleuren NIEUW Black Friday | 799 EUR (FIXED) | business | Roelofarendsveen
Iphone 16 | no amount (FAST_BID) | private | 's-Gravenhage
Enter fullscreen mode Exit fullscreen mode

Four of the ten rows have no amount. Three are FAST_BID (the ad asks for bids) and one is SEE_DESCRIPTION. The two MIN_BID rows carry an amount, which is the starting bid. The three rows marked business are the ones whose listings carry business-profile markers; sellerTypeInferred is the Actor's own label, because the site does not state it.


Understanding your results

Every row carries 36 fields. The ones you will use most:

  • price, priceType, currency: the amount in euros and how the site labels it (FIXED, MIN_BID, FAST_BID, SEE_DESCRIPTION). price is null when there is no amount, never 0.
  • title, descriptionSnippet, url, imageUrls: the ad text (a snippet of about 200 characters), the link and up to four photos.
  • city, country, latitude, longitude: the location as the site gives it.
  • sellerName, sellerId, sellerVerified, sellerWebsiteUrl, sellerTypeInferred: the seller.
  • condition, delivery and attributes: the site's localised wording (Dutch on nl), and the other attributes of the category, such as storage, colour or battery health.
  • isPromoted, isReserved, priorityProduct, traits: promotion and reservation flags.
  • dateText: the placement date as the site words it, for example Vandaag.

One complete row from the run (imageUrls cut to its first entry):

{
    "id": "m2448362351",
    "url": "https://www.marktplaats.nl/v/telecommunicatie/mobiele-telefoons-apple-iphone/m2448362351-iphone-13-128gb",
    "title": "iPhone 13 128gb",
    "descriptionSnippet": "Iphone 13 128gb. Batterij conditie is 76%. Functioneert goed, wel lichte gebruikssporen. (Zie foto's).",
    "price": 175,
    "priceType": "FIXED",
    "currency": "EUR",
    "dateText": "Vandaag",
    "city": "Terneuzen",
    "country": "Nederland",
    "countryCode": "NL",
    "latitude": 51.325071850783,
    "longitude": 3.8383253430983,
    "abroad": false,
    "imageUrls": [
        "https://images.marktplaats.com/api/v1/hz-mp-pro-listing/images/feef3ad5-7b06-4fe7-bffb-f403d6e18ba6?rule=ecg_mp_eps$_82.jpg"
    ],
    "sellerId": 21660597,
    "sellerName": "Yannickmb",
    "sellerVerified": false,
    "sellerWebsiteUrl": null,
    "sellerTypeInferred": "private",
    "categoryId": 1953,
    "parentCategoryId": 820,
    "verticals": [
        "mobile_phones_apple_iphone",
        "telecommunication",
        "barcode-supported"
    ],
    "condition": "Gebruikt",
    "delivery": "Ophalen of Verzenden",
    "attributes": {
        "condition": "Gebruikt",
        "delivery": "Ophalen of Verzenden",
        "color": "Blauw",
        "type": "IPhone 13",
        "battery health": "76 %"
    },
    "traits": [
        "DAG_TOPPER",
        "PACKAGE_FREE"
    ],
    "priorityProduct": "DAGTOPPER",
    "isPromoted": true,
    "isReserved": false,
    "hasPhoneFlag": false,
    "searchCountry": "nl",
    "searchQuery": "iphone",
    "categoryPath": null,
    "searchUrl": "https://www.marktplaats.nl/q/iphone/",
    "scrapedAt": "2026-11-15T19:31:08.089215+00:00"
}
Enter fullscreen mode Exit fullscreen mode

Pro tips

  1. Filter by seller early. sellers: "private" or "business" removes the other group before it reaches your dataset. In the run above, 7 rows were private and 3 business.
  2. Expect different attributes per row. attributes holds whatever the ad carries. In these rows storage appears on 7 of 10, type on 7 and battery health on 1, so use .get() rather than indexing.
  3. requireKeywordMatch trims noise. With it on, a row is kept only if every word of the keyword is in the title or snippet. It is ignored for category paths.
  4. Price bounds drop rows without an amount. As soon as minPrice or maxPrice is set, bidding and "see description" listings are removed, because they have no number to compare.
  5. Each listing is stored once. A listing seen twice in the same run, on another page or under another keyword, produces one row.
  6. Keep the first run small. maxItems: 10 and maxPages: 1 show the shape of the data before you widen the search. A single search reaches up to 167 pages of 30 listings, and the sites can stop serving results sooner.

Troubleshooting

  • Zero rows, status SUCCEEDED. The site answered but nothing matched your filters. The run's status message says so. Loosen the filters or check the keyword.
  • Zero rows, status FAILED. An unknown category path or an invalid input ends the run with a message that says what to fix. Copy category paths from the site you search: on 2ememain.be they are French.
  • Fewer rows than expected. The log ends with Skipped N: reason lines for rows removed by your filters, followed by Pushed N row(s).. Read it with client.log(run.id).get().

Check that your use of the data complies with the site's terms 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 notify your application when a run finishes (webhook docs).

What are you planning to search for on Marktplaats? Drop a comment! 💬

About the author: reventadata builds and publishes data extraction Actors on the Apify Store.

Top comments (0)