# Marktplaats Scraper & New-Listing Monitor + 2dehands (`neverempty/marktplaats-listings-monitor`) Actor

For resellers, deal alerts and price trackers: Marktplaats.nl and 2dehands.be listings by search term, category or pasted URL with price, city, date, photos and attributes. Watch mode returns only listings created since your last check and price drops, never old ads bumped back to the top.

- **URL**: https://apify.com/neverempty/marktplaats-listings-monitor.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** E-commerce, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.90 / 1,000 listing returneds

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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Marktplaats Scraper & New-Listing Monitor (+ 2dehands.be)

Get listings from **Marktplaats.nl**, the biggest classifieds site in the Netherlands, and from its Belgian sister site **2dehands.be**, by search term, category or a pasted search address. One row per listing: title, price in EUR, city, the date the site shows, photos, category, attributes and link. **Watch mode** turns a search into a feed: run it on a schedule and each run returns only the listings created since your watch started that you have not received yet, plus listings whose price went down.

No login, no cookies, no API key. It reads the public search and category pages only.

### What you can do with it

- **Deal alerts**: watch "gazelle orange c7" or a car category every 15 minutes and get only the new listings.
- **Reselling and sourcing**: list a category, filter by price range, and send the rows to a sheet or a bot.
- **Price tracking**: watch mode returns a `price-drop` row with the previous price and the difference when a seller lowers the price.
- **Market research**: how many listings the site shows for a search (`listingsInSearch`), prices, attributes, and how many are paid Dagtopper listings.

### Input

| Field | What it does |
|---|---|
| `site` | `marktplaats` (marktplaats.nl, default) or `2dehands` (2dehands.be). |
| `searchQueries` | Search terms, one per line (`fiets`, `gazelle orange`, `iphone 13`). |
| `categories` | The category part of the address after `/l/`: `auto-s`, or `fietsen-en-brommers/fietsen-dames-damesfietsen`. Each term is searched in each category. |
| `searchUrls` | Or paste a search or category address, such as `https://www.marktplaats.nl/q/fiets/` or `https://www.marktplaats.nl/l/auto-s/f/particulier/10898/` (private sellers only). Filters that are part of the address are kept. |
| `maxResultsPerSearch` | Listings per search, 1 to 5010 (default 30). |
| `minPrice` / `maxPrice` | Price range in EUR, applied to the pages read. |
| `includeAds` | Also return Admarkt ads (paid business ads mixed into each page) and the top block. Off by default. |
| `onlyNew` | Watch mode (see below). |
| `includePriceDrops` | In watch mode, also return price drops (default on). |
| `watchPages` | In watch mode, how many pages (30 listings each) to read per check, 1 to 34 (default 5). |
| `watchName` | Keeps separate memories for separate schedules of the same search. |
| `resetMonitoringState` | Start a watch over. |

Example: `{ "searchQueries": ["fiets"], "maxResultsPerSearch": 30 }`

Watch example: `{ "searchQueries": ["gazelle orange c7"], "onlyNew": true, "watchPages": 5 }` on a schedule.

### Output

One row per listing (`status: "ok"`):

| Field | Meaning |
|---|---|
| `listingId` | The site's listing ID (`m…`; Admarkt ads start with `a`). |
| `title`, `description` | Title and the description snippet shown on the search page. Phone numbers and email addresses written in them are replaced with `[phone removed]` / `[email removed]`. |
| `price`, `priceType`, `currency` | Price in EUR and the site's own price type word: `FIXED` (asking price), `MIN_BID` (bidding from this amount), `FAST_BID` (bidding, no amount), `SEE_DESCRIPTION`. `price` is null when the site shows no amount. |
| `previousPrice`, `priceChange` | Watch mode, `price-drop` rows only. |
| `city`, `countryCode` | Where the listing is, as shown (city level). |
| `dateShown`, `listedDate` | The date text on the card (`Vandaag`, `Gisteren`, `5 okt 26`) and the same date as `YYYY-MM-DD` (Amsterdam time). The site moves this date when a listing is renewed or promoted. |
| `imageUrl`, `imageUrls` | Photos. |
| `attributes` | Condition, delivery, brand and the other attributes shown, as the site names them. |
| `isPromoted` | `true` for paid Dagtopper listings. |
| `isAdmarkt` | `true` for Admarkt ads (only with `includeAds`). |
| `isBusiness` | `true` for Admarkt ads, sellers with a website, and lists filtered to businesses; `false` for lists filtered to private sellers (`/f/particulier/10898/`); otherwise `null` (not shown on the search page). |
| `isReserved`, `sellerVerified` | As shown on the site. |
| `url` | Link to the listing. |
| `positionInSearch`, `listingsInSearch` | Position in the list read, and the total the site shows for the search. |
| `changeType` | Watch mode: `first-check`, `new` or `price-drop`. |
| `site`, `searchQuery`, `searchCategory`, `searchCategoryName`, `searchUrl`, `watchName`, `checkedAt` | Which search the row came from. |

Seller names and seller IDs are not returned.

In the pages read while building this Actor (232 listings, 11 pages, October 2026), 86% had a shown price and 100% had a city, a date and a photo; 69% were paid Dagtopper listings.

### Watch mode, and what "new" means here

The first run returns the listings it reads and remembers them (`first-check`). Every later run returns:

- `new`: a listing you have not received that was **created after the watch started**. The site numbers listings in the order they are created, so a listing whose number is higher than the highest number seen at the first check is new. An old listing that a seller bumped back to the top, or that rises in the order, is not called new.
- `price-drop`: a listing you already received whose price went down. Price rises are remembered, not returned.

A check that finds nothing returns one free `no-new-listings` row and is charged as one small `search-checked` event. Listings that could not be returned because of your run's maximum charge are not marked as delivered, and the next run returns them. One limit: a listing created a few hours before your first check can have a higher number than any listing that check read, and then comes back once as `new`.

### The order is the site's, and it matters

The site lists search results with **paid Dagtopper listings first** and the rest by relevance, **not by date**. Sorting by date and price filters are applied by the site's page in your browser (after `#` in the address) through an internal search API that the site's robots.txt does not allow, so this Actor does not use them, and addresses with `?` or `#` filters are refused instead of being silently ignored.

For watch mode this means a new listing can appear on any page. A watch reads `watchPages` pages per check; if the site shows more listings than that, the first check says so in a free `watch-coverage` row. Use a narrow search (more words, a category, a filter in the address) so the pages read cover the whole list.

### Rows that explain instead of data (never charged)

`no-results`, `not-found` (unknown category, or the site answered with a different list), `none-in-price-range`, `no-new-listings`, `watch-coverage`, `incomplete` (a later page could not be read), `blocked` (the site showed a check page; this Actor does not solve or bypass it), `unreadable`, `not-allowed-by-robots-txt`, `bad-input`, `budget-reached`, `more-not-returned`. Each has a `note` that says what happened.

### Limits

- The site serves at most 167 pages (5,010 listings) per search.
- Up to 10 searches per run; pages are read one by one with a pause between them.
- The description is the snippet on the search page, not the full listing text.
- The site's robots.txt is read on every run, and pages it does not allow are not requested.

### Thanks for using this Actor

We build these tools for people who run them every day, and we improve them from what users tell us.

- **Missing a field, or need another filter?** Tell us in the **Issues** tab. If the data is there, we add it.
- **Found a bug or a wrong value?** Post the run ID in the **Issues** tab. Wrong data is the thing we fix first.

If this Actor saved you time, a short review helps other people find it.

# Actor input Schema

## `site` (type: `string`):

Which classifieds site to search: marktplaats.nl (Netherlands) or 2dehands.be (Belgium, same company and same page layout). Applies to search terms and categories; pasted search URLs use their own site.

## `searchQueries` (type: `array`):

What to search for, one term per line (for example fiets, gazelle orange, iphone 13). Each term is searched in every category below. Leave empty to list whole categories instead; if everything is empty, the example term fiets is searched.

## `categories` (type: `array`):

Category names as they appear in the site's address after /l/, one per line: auto-s for marktplaats.nl/l/auto-s/, or fietsen-en-brommers/fietsen-dames-damesfietsen for a subcategory. Leave empty to search all categories.

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

Or paste the address of a search or category page, such as https://www.marktplaats.nl/q/fiets/ or https://www.marktplaats.nl/l/auto-s/f/particulier/10898/ (filters that are part of the address are kept). Addresses with ? or # filters are refused, because the site applies those in your browser only.

## `maxResultsPerSearch` (type: `integer`):

How many listings to return for each search, from 1 to 5010. The site shows 30 per page and at most 167 pages per search. In watch mode, the most rows one check returns per search.

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

Return only listings priced at or above this many euros. Applied to the pages read. Listings without a shown price (bidding, see description) are left out when a price range is set. Empty means no minimum.

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

Return only listings priced at or below this many euros. Applied to the pages read. Empty means no maximum.

## `includeAds` (type: `boolean`):

The site mixes Admarkt ads (paid business ads, IDs starting with a) into each page and shows a top block above it. Off by default, so you get the listings only. Paid Dagtopper listings are always included and marked isPromoted.

## `onlyNew` (type: `boolean`):

Run the same search on a schedule. The first run returns the listings it reads and remembers them. Every later run returns only listings created since the watch started that you have not received yet, and listings whose price went down. A check that finds nothing is charged as one small search-checked event instead of rows.

## `includePriceDrops` (type: `boolean`):

In watch mode, also return listings you already received when their price went down, with the previous price and the difference. Price rises are remembered but not returned.

## `watchPages` (type: `integer`):

How many pages (30 listings each) a watch reads per search on each check, from 1 to 34. The site does not sort by date, so a new listing can sit on any page; use a narrow search (a category, more words) so these pages cover the whole list.

## `watchName` (type: `string`):

Optional. A name for this watch (letters, digits, dot, dash, underscore). Two schedules with the same search but different names keep separate memories. Empty uses one shared default watch.

## `resetMonitoringState` (type: `boolean`):

Start this watch over: the run is treated as the first check again. Off by default.

## Actor input object example

```json
{
  "site": "marktplaats",
  "searchQueries": [
    "fiets"
  ],
  "maxResultsPerSearch": 30,
  "includeAds": false,
  "onlyNew": false,
  "includePriceDrops": true,
  "watchPages": 5,
  "resetMonitoringState": false
}
```

# Actor output Schema

## `results` (type: `string`):

One row per Marktplaats or 2dehands listing: listing ID, title, price in EUR with the site's price type, description snippet, city, the date shown and that date as YYYY-MM-DD, photos, category, attributes, Dagtopper and Admarkt flags, business flag when known, link and position. Watch mode marks rows first-check, new or price-drop (with the previous price). A search with no listings, nothing new, an unknown category, a page robots.txt does not allow, a refused request or a run that hit its maximum charge comes back as a free row that says why.

# 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 = {
    "searchQueries": [
        "fiets"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/marktplaats-listings-monitor").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 = { "searchQueries": ["fiets"] }

# Run the Actor and wait for it to finish
run = client.actor("neverempty/marktplaats-listings-monitor").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 '{
  "searchQueries": [
    "fiets"
  ]
}' |
apify call neverempty/marktplaats-listings-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/marktplaats-listings-monitor"
        }
    }
}
```

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/tzuvlCDSuNbY83j9j/builds/D8NxEIlHmeiCaKFR1/openapi.json
