# 2dehands & 2ememain – Belgium Classifieds & Seller Reviews (`abotapi/2dehands-2ememain-scraper`) Actor

Scrape classifieds from 2dehands.be and 2ememain.be by keyword, filters, or URLs. Returns title, price, location, images, seller details, category, and full tag-based seller reviews. Supports sort, category, price filters, listing URLs, and search URLs.

- **URL**: https://apify.com/abotapi/2dehands-2ememain-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** E-commerce, Developer tools, Lead generation
- **Features:** Limited permissions
- **Stats:** 10 total users, 0 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: 3.00 out of 5 stars

## Pricing

from $1.25 / 1,000 results

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

## 2dehands & 2ememain Scraper: Listings, Prices, Sellers and Reviews from Belgium's Classifieds

2dehands & 2ememain Scraper turns Belgium's two largest general classifieds marketplaces into structured data. 2dehands.be and 2ememain.be share one catalogue (the same listings in Dutch and French), so one actor covers both. Search by keyword with price, sort and category filters, or paste listing and search links directly. Get normalized listing records: id, title, description, price and price type, full location, images, seller and, when you turn on full detail, view and favorite counts, item attributes, shipping and bidding info, plus the seller's full tag-based reviews. Export to JSON, CSV or Excel, or pull the results straight into your app through the API.

### Why This Scraper?

- **Two ways in.** Search by keyword with price, sort and category filters, or paste listing and search links directly. No need to build URLs by hand.
- **One catalogue, two languages.** Covers 2dehands.be (Dutch) and 2ememain.be (French), the same Belgian marketplace under both names, from a single run.
- **Optional full detail.** Turn on listing detail to add view and favorite counts, full category, item attributes, vehicle specs, shipping and bidding info, and seller profile fields.
- **Full tag-based reviews.** Seller feedback on these sites is structured, not free text. Every score, characteristic tag, reviewed item and review direction is captured in full.
- **Exact price filtering.** Minimum and maximum price bounds are applied to each listing's real price, not an approximate facet, so filtered results are always exact.
- **Built for monitoring.** Incremental mode tracks a saved search over time and returns only what changed.
- **Resume large pulls.** Continue an interrupted run from its previous dataset without paying twice for listings already collected there.

### Use Cases

- **Resale and pricing research:** track asking prices, price types and category supply across Belgium over time.
- **Marketplace intelligence:** benchmark seller activity, verification status and buyer feedback for competitor or vendor research.
- **Lead generation:** surface active sellers and dealers with contact and profile context for prospecting.
- **Data engineering:** feed normalized listing records into a warehouse, dashboard or alerting pipeline on a schedule.
- **Category and vehicle tracking:** monitor a specific model, brand or category and get notified only when something new appears or a price changes.

### Data You Get

> Sample shape: values are illustrative placeholders, not from a live record.

| Field | Example |
| --- | --- |
| `type` | `"listing"` |
| `itemId` | `"2330775046"` |
| `title` | `"Sample bicycle, well maintained"` |
| `url` | `"https://www.2dehands.be/v/fietsen/mountainbikes/m2330775046-sample-bicycle"` |
| `price` / `priceCents` | `150` / `15000` |
| `priceType` | `"FIXED"` (also `RESERVED`, `MIN_BID`, `FAST_BID`, `ON_REQUEST`, `FREE`, `EXCHANGE`, `SEE_DESCRIPTION`) |
| `city` / `countryName` | `"Gent"` / `"Belgium"` |
| `latitude` / `longitude` | `51.05` / `3.72` |
| `date` | `"2025-05-01T10:00:00Z"` |
| `imageUrl` / `imageUrls` | the first image, then the full list of image URLs |
| `sellerId` / `sellerName` | `"u-000111"` / `"sample_seller"` |
| `sellerVerified` | `true` |
| `categoryId` | `2100` |
| `traits` / `verticals` | listing flags and marketplace segment tags, straight from the source |
| `listingId` / `country` | `"m2330775046"` (same value as `itemId`) / `"BE"` |
| `postedDate` / `postedAt` | `"2025-05-01"` / `"2025-05-01T00:00:00.000Z"`: the site only publishes the posting day ("Vandaag", "Gisteren"), so these are day-precise, midnight UTC |
| `sourceSite` | `"2dehands.be"` (or `"2ememain.be"`) |
| `scrapedAt` | `"2025-05-01T10:05:00Z"` |
| `changeType` | `"NEW"` (incremental mode only; also `UPDATED`, `UNCHANGED`, `REAPPEARED`, `EXPIRED`) |
| `changedFields` | `["price"]` (incremental mode only) |
| `firstSeenAt` / `lastSeenAt` | incremental mode only, first and most recent time this listing was seen |
| `viewCount` / `favoritedCount` | `128` / `4` (populated when Fetch full listing details is on) |
| `sellerRating` / `sellerReviewCount` | `4.8` / `23` (populated when Fetch full listing details is on) |

When Fetch full listing details is on, each record also gets `categoryName`, `categoryFullName`, `categoryParentName`, `attributes[]` (plus each attribute flattened to its own `attr_<key>`), `carDetails` for vehicle listings, `shippingDeliveryType`, `biddingEnabled` and `bidCount`, `adType`, `buyItNowEnabled`, `isReserved`, `highlights[]`, `detailImageUrls[]`, and seller profile fields (`sellerType`, `sellerActiveYears`, `sellerProfileUrl`, `sellerContactOptions[]`, `sellerPaymentMethod`, `sellerSmbVerified`, `sellerHasPhone`, `sellerResponseRate`, `sellerResponseSpeed`, `sellerResponseLabel`, `sellerReviewSystem`). When Include seller reviews is also on and the seller actually has reviews, the record additionally carries `sellerReviewsSummary` (average score, rating distribution, positive and negative characteristic tags) and a `reviews[]` array, each entry with `reviewId`, `author`, `date`, `score`, `direction`, `reviewedItemTitle`, `reviewedItemCategory`, `isSeller` and `feedbackTags[]`.

### How to Use

1. Pick a **mode**: `search` (keyword plus filters) or `url` (paste listing or search links).
2. For search mode, add keywords and set the domain, price range, sort and category filters you want. For url mode, paste listing or search links; the site is taken from each link, price bounds still apply, sort and category are used only when the link doesn't already set them, and pagination continues from wherever the link points.
3. Decide whether to turn on **Fetch full listing details** and **Include seller reviews**, and set **Max listings** (the run cap) and, if needed, **Max pages**.
4. Click **Start**, then download the dataset as JSON, CSV or Excel, or read it through the API.

**Keyword search with a price floor:**

```json
{
  "mode": "search",
  "queries": ["bmw"],
  "domain": "2dehands.be",
  "sort": "price_asc",
  "minPrice": 2000,
  "maxItems": 50
}
```

**Full detail plus reviews:**

```json
{
  "mode": "search",
  "queries": ["iphone"],
  "fetchDetails": true,
  "includeReviews": true,
  "maxReviews": 30,
  "maxItems": 25
}
```

**Behaviour change:** `startUrls` now has a default (one example link per supported shape). In url mode, leaving the field out scrapes those examples; to scrape your own links, set the field. Category links such as `/l/auto-s/` now keep their category filter instead of running an unfiltered site-wide search. Pasted links keep the filters the site API understands: sort, posted-since (`offeredSince`), language (`nl-BE` or `fr-BE`), condition and delivery (the Dutch or the 2ememain French label), facet ids (`#f:30`, or several at once such as `#f:30,31`; every id in a list must be a condition or delivery facet the actor knows, otherwise the whole `f` entry is ignored and logged), the search keyword (`#q:bmw`, ignored when the link already has a different keyword) and the start position (`#offset:60`, which counts like a later page), price bounds (`PriceCentsFrom`/`PriceCentsTo` in the hash, or `priceFrom`/`priceTo` in euros in the query string) and the page (`currentPage`). Any other key, query parameter or unknown value is ignored and the run logs which one. Facet ids (`#f`) are category-specific: 30, 31 and 32 are condition values and 33 and 34 are delivery values, but a category that lacks an id (for example cars have no 31, and some categories have no condition facet at all) makes the site ignore it without any message, so the result can be broader than the link on the site shows. Links that point past the first page (`/p/2/`, `?currentPage=2`, `#currentPage:2`) never count as a complete scan, so they cannot mark tracked listings as expired. Because category and hash-filter links used to run unfiltered, users with **Emit expired listings** on who track such links can see one batch of `EXPIRED` rows on the first run after this update, for tracked listings outside the new category or filter. Incremental baselines saved before multi-value facet ids and the `q:`/`offset:` link keys were supported keep ignoring them (the run logs it), because applying them would narrow the tracked set and mark listings that are still online as `EXPIRED`; use a new State key or Reset state to apply them. New baselines apply them from the first run. Postcode, condition and delivery filters do not apply to seller-profile or single-listing links (the run logs a notice when they are set).

**Paste links across both sites:**

```json
{
  "mode": "url",
  "startUrls": [
    "https://www.2dehands.be/q/fiets/",
    "https://www.2ememain.be/q/velo/"
  ],
  "maxItems": 40
}
```

**Recurring monitoring of a saved search:**

```json
{
  "mode": "search",
  "queries": ["fiets"],
  "domain": "2dehands.be",
  "incrementalMode": true,
  "emitExpired": true
}
```

**What happens with little or no input.** If you give only links in **Listing or search URLs** (leaving Mode at its default), the run reads those links as url mode. A category (Category ID) counts as a target on its own: a keyword-less category search runs as it always did, and pasted links next to a category stay in search mode. If you give no keyword, no category, no link and no listing id at all, the run searches the example keyword "bmw" so you still get rows; with Incremental mode on, that example run does not read or save any state and never marks anything expired. If you set a **State key** with Incremental mode but give nothing to track, the run stops at the start with a message asking for keywords, links or listing ids.

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/2dehands-2ememain-scraper").call(run_input={"mode": "search", "queries": ["bmw"], "maxItems": 20})
for listing in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(listing["title"], listing["price"], listing["url"])
```

JavaScript:

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/2dehands-2ememain-scraper').call({ mode: 'search', queries: ['bmw'], maxItems: 20 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the **Integrations** tab.

#### One cap rule

**Max listings** is the single cap on a run. **Max pages** defaults high and stops once Max listings is reached. Set Max pages only when you also want a hard per-search page limit.

#### Resume and recurring updates

- **Resume from a previous run** (`resumeFromRunId`) continues one specific interrupted or large pull: paste a run or dataset id and this run skips listings already collected there, so you only pay for what's new.
- **Incremental mode** (`incrementalMode`) is for scheduling this same search again and again, for example daily. The actor remembers the previous run of the same search itself, so you never paste a run id. The first run returns everything as `NEW`. Later runs return only `NEW` and `UPDATED` listings by default; unchanged listings are suppressed and not billed unless **Emit unchanged** is on. `REAPPEARED` and `EXPIRED` are both off by default and only appear once you turn on **Emit expired listings**: a run that fully scans the tracked search with that setting on marks any previously-tracked listing that is no longer found as `EXPIRED`, and only after that has happened at least once can that same listing later come back as `REAPPEARED`. A capped or resumed run never marks anything expired, since it did not prove the listing is really gone. The same holds when the site answers with an empty results page that its own result count does not confirm as the end: the run re-reads that page once after a few seconds, and if it is still empty it treats the scan as incomplete, logs why, marks nothing as expired and keeps the saved state as it was. `stateKey` names or deliberately shares one monitoring campaign; left empty, the actor derives a key from the mode and search settings automatically.

### Send results into your apps (MCP connectors)

Optionally pipe a condensed summary of each result 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.

1. Authorize a connector once under **Apify > Settings > Integrations** (Notion, Linear, Airtable, or Apify).
2. Select it in the **"Pipe results into your apps"** input field. If the picker is empty, you haven't authorized a connector yet.
3. For **Notion**, also set the Notion parent page field to the page where items should be created.

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` (keyword plus filters) or `url` (paste listing or search links). Left at search with links but no keyword or listing id, the links are read as url mode; with nothing at all (no keyword, category, URL or listing id), an example search for "bmw" runs. |
| `queries` | array | none (prefill: `["bmw"]`) | Search keywords, one per line. Each runs a separate search. |
| `startUrls` | array | one example link per supported shape (15) | Links for url mode: keyword search, paginated search (`/p/2/`, `?currentPage=2`), hash filters and sort (including price bounds and page), category pages (`/l/auto-s/`, `/l/auto-s/q/bmw/`, `/l/auto-s/bmw/`), listing pages, seller profiles (`/u/name/id/`) and `/lrp/api/search` links, on 2dehands.be or 2ememain.be. Other links are skipped with a warning. A listing that several links or searches point to is collected, billed and enriched once. |
| `domain` | string | `2dehands.be` | `2dehands.be` (Dutch) or `2ememain.be` (French). Same catalogue. |
| `minPrice` | integer | `0` | Keep only listings priced at or above this amount in EUR. `0` keeps all. |
| `maxPrice` | integer | `0` | Keep only listings priced at or below this amount in EUR. `0` sets no upper bound. |
| `searchQuery` | string | none | Alias of `queries`; all query fields are combined. |
| `searchQueries` | array | none | Alias of `queries`; all query fields are combined. |
| `listingIds` | array | none | Fetch these exact listings (bare ids such as `m2330775046` or listing links) instead of searching. Keyword and every filter field are ignored. |
| `postcode` | string | none | Belgian 4-digit postcode. An invalid postcode stops the run instead of returning unfiltered results. |
| `radiusKm` | integer | `25` | Radius around the postcode, 1 to 300 km. |
| `conditions` | array | none | Any of `new`, `as-new`, `used`, `refurbished`, `not-working` (OR-ed). Not every category offers every condition. |
| `delivery` | string | `any` | `any`, `pickup`, or `shipping` (shipping includes listings offering both). |
| `buyItNowOnly` | boolean | `false` | Keep only fixed-price listings (drops bidding, swap and on-request). Dropped rows are never billed. |
| `postedAfter` | string | none | `YYYY-MM-DD`: keep listings posted on or after this day (Europe/Brussels, day-precise). Rows whose date cannot be read are kept. With a newest-first sort, the walk stops once a page is entirely older than this day. |
| `postedBefore` | string | none | `YYYY-MM-DD`: keep listings posted on or before this day (Europe/Brussels, day-precise). Rows whose date cannot be read are kept. |
| `priceMinEur` | integer | none | Alias of `minPrice` (the canonical field wins when both are set). |
| `priceMaxEur` | integer | none | Alias of `maxPrice` (the canonical field wins when both are set). |
| `sortBy` | string | none | Alias of `sort`: `date`, `price_asc`, `price_desc`, `relevance`. Wins over `sort` when set. |
| `scrapeDetails` | boolean | none | Alias of `fetchDetails`. |
| `proxyConfiguration` | object | none | Alias of `proxy`. |
| `onlyNewListings` | boolean | `false` | Returns (and bills) only listings this search has not shown before, using the same remembered state as Incremental mode; skipped rows are never pushed or billed. |
| `resetState` | boolean | `false` | Forgets that remembered state first, so everything counts as new. |
| `sort` | string | `optimized` | `optimized`, `date_desc`, `date_asc`, `price_asc`, or `price_desc`. |
| `categoryId` | integer | `0` | Restrict to a top-level category by numeric id. `0` searches all categories. |
| `fetchDetails` | boolean | `false` | Open each listing page for view/favorite counts, full category, seller profile, item attributes, shipping, bidding, and reviews. Billed as a detail-enrichment surcharge. |
| `includeReviews` | boolean | `true` | When details are on, capture the seller's tag-based reviews and rating summary. |
| `maxReviews` | integer | `20` | Cap on individual reviews captured per listing's seller. |
| `maxItems` | integer | `20` | The run cap: global max listings returned. `0` means unlimited, clamped to 1000. |
| `maxPages` | integer | `200` | Per-search page limit (30 listings per page). Stops at Max listings regardless. |
| `proxy` | object | Residential, country BE | Connection settings. |
| `mcpConnectors` | array | none | Optional: send a summary of each listing to apps you authorized under Integrations. |
| `notionParentPageUrl` | string | none | Notion connector only: page under which items are created. |
| `maxNotifyListings` | integer | `50` | Cap on items written to each connector per run. Does not affect the dataset. |
| `resumeFromRunId` | string | none | Continue one specific previous run or dataset without re-collecting its listings. |
| `incrementalMode` | boolean | `false` | Turn on for recurring monitoring; return only new and changed listings on later runs. |
| `stateKey` | string | none | Name or deliberately share one monitoring campaign's saved state. |
| `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, after a complete scan. |
| `ignoreFieldsForChanges` | string\[] | none | Incremental mode only. Field names or dotted paths (for example `seller.name`) that do not count as a change when only they differ; they stay in the output. View, favourite and bid counters are always ignored. |

### Output Example

> Sample shape: values are illustrative placeholders, not from a live record.

```json
{
  "type": "listing",
  "itemId": "2330775046",
  "title": "Sample bicycle, well maintained",
  "description": "Well maintained mountain bike, 28 inch, barely used.",
  "url": "https://www.2dehands.be/v/fietsen/mountainbikes/m2330775046-sample-bicycle",
  "price": 150,
  "priceCents": 15000,
  "priceType": "FIXED",
  "city": "Gent",
  "countryName": "Belgium",
  "latitude": 51.05,
  "longitude": 3.72,
  "date": "2025-05-01T10:00:00Z",
  "imageUrl": "https://sample.example/img/1.jpg",
  "imageUrls": ["https://sample.example/img/1.jpg", "https://sample.example/img/2.jpg"],
  "sellerId": "u-000111",
  "sellerName": "sample_seller",
  "sellerVerified": true,
  "categoryId": 2100,
  "categoryName": "Bikes",
  "sourceSite": "2dehands.be",
  "viewCount": 128,
  "favoritedCount": 4,
  "sellerRating": 4.8,
  "sellerReviewCount": 23,
  "sellerReviewsSummary": {
    "averageScore": 4.8,
    "numberOfReviews": 23,
    "positiveCharacteristics": ["Fast handling", "Friendly contact"],
    "negativeCharacteristics": []
  },
  "reviews": [
    {
      "reviewId": "r-000",
      "author": "sample_buyer",
      "date": "2025-05-01T10:00:00Z",
      "score": 5,
      "direction": "B2S",
      "reviewedItemTitle": "Sample bicycle",
      "reviewedItemCategory": "Bikes",
      "isSeller": true,
      "feedbackTags": [
        { "text": "Fast handling", "isPositive": true },
        { "text": "Friendly contact", "isPositive": true }
      ]
    }
  ],
  "scrapedAt": "2025-05-01T10:05:00Z"
}
```

### Plan Requirement

The default connection setting is the Residential proxy group with country BE, the most reliable choice for these two Belgian sites. If you choose Automatic (no proxy group) in Connection, the run uses the Automatic group and never upgrades itself to Residential unless that connection fails; your own Connection choice is respected.

- **Paid Apify plan:** the default works as is. A refused request is retried on a fresh connection, and if Residential is unavailable the run continues on the Automatic group.
- **Free Apify plan:** Apify free plans do not include residential proxies. The run logs a notice that the selected connection is unavailable and tries the Automatic group and a backup connection. The backup connection is currently refused by these sites, so a free-plan run normally ends as failed with a message to upgrade or supply your own proxy URLs, rather than returning an empty result. To get data, upgrade your Apify plan or supply your own proxy URLs.
- **Your own proxy URLs:** used as given, with no backup connection added.
- **Proxy switched off:** requests go out directly from the run and no backup connection is added.

A run that cannot load any results page ends as failed with a message, never as an empty success. The message tells the causes apart: the proxy connection was refused for the account (a paid plan or valid own proxy credentials are needed), residential proxies are unavailable on a free plan and the backup connection could not load the pages, or no results page could be loaded at all (a connection problem, try again later). If a run returns no results, try a fresh run or check that the connection is set to Residential country BE.

### FAQ

#### How much does it cost?

Billing has three parts: a small charge when the run starts, a charge per listing returned (rows dropped by filters, incremental suppression or Buy it now only are never pushed or billed), and a detail-enrichment charge per enriched listing only when **Fetch full listing details** is on. That enrichment covers the listing detail fields, the seller profile and, with **Include seller reviews** on, the seller's reviews. The **Pricing** tab shows the current rates. Use **Max listings** to cap the cost of any run. Seller reviews are only fetched together with full listing details, never on their own, and **Max reviews per listing** is capped at 200, so a run cannot fan out into unbounded review scraping.

#### Is it legal to scrape 2dehands and 2ememain?

This actor collects only publicly available classifieds data. You are responsible for how you use it: follow the sites' terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Listing photos and seller-provided text may carry their own rights.

#### Can I get only new or changed listings on a schedule?

Yes. Schedule the actor from the **Schedules** tab with the same search settings and **Incremental mode** turned on. The first run returns everything as `NEW`; later runs return only `NEW` and `UPDATED` listings by default, and unchanged ones are not billed. Turn on **Emit expired listings** if you also want to know when a tracked listing disappears, and only after that has fired once can a listing later come back marked `REAPPEARED`.

#### What's the difference between Resume and Incremental mode?

**Resume from a previous run** continues one specific interrupted or large pull using a pasted run or dataset id, so this run appends only the remaining new listings. **Incremental mode** is for running the exact same search again and again on a schedule and getting only what changed; the actor tracks its own state and you never paste an id. Combining incremental mode with resumeFromRunId only works to bootstrap a brand-new monitoring campaign; if that search already has saved incremental state, the run fails with a clear message rather than mixing the two.

#### Why did my run fail instead of returning an empty dataset?

A few situations fail on purpose rather than quietly returning nothing: a `resumeFromRunId` that doesn't match a run or dataset this account can read, combining incremental mode with resumeFromRunId when that search already has saved state, a url-mode run where none of the links could be used, or no usable connection could be set up at the start of the run. In those cases the run stops with a clear message so you know to fix the input rather than assume there were no listings. If individual requests are refused during the scrape, or the connection succeeds but no listings match your filters (for example, a very narrow price range), the run still finishes successfully and returns zero or fewer items with a warning suggesting you re-run or broaden the filters.

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

<table>
<tr><td>🏷️ <a href="https://apify.com/abotapi/marktplaats-nl-scraper"><b>Marktplaats.nl Scraper</b></a><br>Scrape Marktplaats.nl listings by keyword or URL. Extract title, price, condition...</td><td>🏷️ <a href="https://apify.com/abotapi/leboncoin-fr-scraper"><b>Leboncoin Scraper</b></a><br>Scrape Leboncoin.fr classifieds across cars, property, jobs, electronics, furniture and...</td></tr>
<tr><td>🏷️ <a href="https://apify.com/abotapi/gumtree-uk-scraper"><b>Gumtree.com Scraper</b></a><br>Scrape Gumtree.com classifieds across motors, property, jobs, services, pets and goods...</td><td>🏷️ <a href="https://apify.com/abotapi/olx-scraper"><b>OLX Scraper</b></a><br>Fast OLX scraper across 9 countries, including Poland, Romania, Portugal, Ukraine...</td></tr>
<tr><td>🏷️ <a href="https://apify.com/abotapi/gumtree-au-scraper"><b>Gumtree AU Scraper</b></a><br>Scrape gumtree.com.au classifieds across every vertical: for-sale goods, motors, real...</td><td>🏷️ <a href="https://apify.com/abotapi/finn-no-jobs-scraper"><b>FINN.no Jobs Scraper</b></a><br>Scrape active FINN.no job listings by search filters or URL. Extract 30+ fields including...</td></tr>
</table>

👉 [Browse all abotapi scrapers](https://apify.com/abotapi)

### 💬 Support

- 🐞 **Found a bug or a missing field?** Open a ticket on the [Issues tab](https://apify.com/abotapi/2dehands-2ememain-scraper/issues/open). 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.

# Actor input Schema

## `mode` (type: `string`):

search = build a search from a keyword plus filters. url = paste one or more listing or search URLs and walk them forward. If you fill only Listing or search URLs (and no keywords, category or listing ids), they are read as url mode; with no keyword, category, URL or listing id at all, an example search for "bmw" runs and Incremental mode does not read or save state for it.

## `queries` (type: `array`):

Product keywords such as "bmw", "iphone", or "fiets". Each line runs a separate search.

## `searchQuery` (type: `string`):

Single-keyword alias of Search keywords; both are combined.

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

Alias of Search keywords; both are combined.

## `listingIds` (type: `array`):

Fetch these exact listings (for example m2427421626) instead of searching. When filled, the keyword and filter fields are ignored. Accepts the bare id or a listing URL.

## `startUrls` (type: `array`):

One or more URLs (url mode). Supported: keyword search (/q/bmw/), paginated search (/p/2/ or ?currentPage=2), hash filters and sort (#sortBy:SORT_INDEX|sortOrder:DECREASING|offeredSince:Today|PriceCentsFrom:5000|PriceCentsTo:20000|currentPage:3), facet ids (#f:30 or #f:30,31; ids are category-specific, 30-32 are condition and 33-34 delivery values, and an id the category lacks is ignored by the site so the result can be broader than asked), keyword (#q:bmw) and start position (#offset:60) in the hash (baselines saved before these were supported keep ignoring them: use a new State key or Reset state to apply them), category pages (/l/auto-s/, /l/auto-s/q/bmw/, /l/auto-s/bmw/), listing pages (/v/...), seller profiles (/u/name/id/), on 2dehands.be or 2ememain.be, and /lrp/api/search links. Other links (mobile host, bare /v/<id>) are skipped with a warning. Filters above apply to search links except postcode, condition and delivery, which do not apply to seller-profile or listing links; pagination starts at the page implied by the URL and walks forward.

## `domain` (type: `string`):

Which Belgian site to search. Both share the same catalogue and API.

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

Keep only listings priced at or above this amount (applied to the listing price). Leave at 0 to keep all. Narrows results.

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

Keep only listings priced at or below this amount. Leave at 0 for no upper bound. Narrows results.

## `priceMinEur` (type: `integer`):

Alias of Minimum price; Minimum price wins when both are set.

## `priceMaxEur` (type: `integer`):

Alias of Maximum price; Maximum price wins when both are set.

## `sort` (type: `string`):

Order the results by relevance, newest, oldest, or price.

## `sortBy` (type: `string`):

Alias of Sort: date = newest first, relevance = best match. When set it wins over Sort.

## `categoryId` (type: `integer`):

Restrict the search to a top-level category by its numeric id. Leave at 0 to search all categories.

## `postcode` (type: `string`):

4-digit postcode (for example 2000). Keeps only listings within the radius below. An invalid postcode stops the run instead of silently returning unfiltered results.

## `radiusKm` (type: `integer`):

1 to 300 km, only used together with a postcode. Defaults to 25 km when empty.

## `conditions` (type: `array`):

Keep only listings in any of these conditions. Empty keeps all. Not every category offers every condition.

## `delivery` (type: `string`):

any, pickup (Ophalen) or shipping (Verzenden; includes listings offering both).

## `buyItNowOnly` (type: `boolean`):

Drop bidding, swap and on-request listings and keep only listings with a fixed price. Dropped rows are never charged.

## `postedAfter` (type: `string`):

YYYY-MM-DD (or an ISO timestamp, date part used). The site only publishes the posting day, so the window is day-precise (Europe/Brussels). Listings whose date cannot be read are kept.

## `postedBefore` (type: `string`):

YYYY-MM-DD (or an ISO timestamp, date part used). Day-precise like Posted on or after.

## `fetchDetails` (type: `boolean`):

Open each listing page to add view/favorite counts, full category, seller type and profile, item attributes, shipping, bidding, and the seller's tag-based reviews. Adds requests per listing (billed as a detail-enrichment surcharge). Turn off to collect only the search-card fields.

## `scrapeDetails` (type: `boolean`):

Alias of Fetch details; either one switches it on.

## `includeReviews` (type: `boolean`):

When details are on, capture the seller's full tag-based reviews (score, positive/negative characteristic tags, reviewed item, reviewer, date) plus a rating summary. Only fetched for sellers that have reviews.

## `maxReviews` (type: `integer`):

Cap on the number of individual reviews captured per listing's seller.

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

Global cap on the number of listings to return across all searches. This is THE cap - Max pages defaults to unlimited and stops at this number. 0 means unlimited (clamped to 1000).

## `maxPages` (type: `integer`):

Per-search page limit (30 listings per page). Leave empty for the built-in hard cap of 200 pages per search; the run stops at Max listings first.

## `proxy` (type: `object`):

Apify Residential proxy set to country BE is recommended for the highest reliability, since both sites are Belgian. Some datacenter exit IPs may be geo-restricted.

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

Alias of Proxy; when set it wins over Proxy.

## `resumeFromRunId` (type: `string`):

Paste a previous run ID or dataset ID to continue a large pull without returning or charging for listings already collected there. Use this after an interrupted run, or to keep extending one walk-all pull across separate runs. For recurring daily monitoring of the same search, use Incremental mode below instead.

## `incrementalMode` (type: `boolean`):

Turn this on for daily or recurring monitoring. The first run returns all matching listings as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED listings. Turn on "Emit unchanged" or "Emit expired" only when you also want those listings returned (and billed). State is kept separately per mode + search/URL/filter configuration; use State key when you want to name or deliberately share a monitoring campaign. To continue one specific interrupted run instead, use Resume from a previous run above.

## `stateKey` (type: `string`):

Optional. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently-configured runs. Leave empty to let the actor derive a key automatically from the mode + search/filter settings — different searches then never mix state with each other.

## `emitUnchanged` (type: `boolean`):

Off by default. Turn on to also return listings that have not changed since the last run, marked UNCHANGED. This returns — and bills — extra rows you already have, so leave it off unless you specifically want the full snapshot every run.

## `emitExpired` (type: `boolean`):

Off by default. Turn on to also return listings that were present in a previous run but are no longer found, marked EXPIRED. Only produced once a run has fully scanned the tracked search — not when Max listings capped it or when Resume was used. This returns — and bills — extra synthetic rows, so leave it off unless you need expiry tracking.

## `onlyNewListings` (type: `boolean`):

Emit only listings this search has not shown before (remembered between runs). Rows skipped as already seen are never pushed or charged. Uses the same remembered state as Incremental mode.

## `resetState` (type: `boolean`):

Clears the remembered state for this search before the run (only with Incremental mode or Only listings not seen before).

## `ignoreFieldsForChanges` (type: `array`):

Incremental mode only. Field names (or dotted paths such as seller.name) that should NOT make a listing count as UPDATED when only they change. They stay in the output. View, favourite and bid counters are always ignored already.

## `mcpConnectors` (type: `array`):

Optionally send a condensed summary of each result into the apps you already use, via Model Context Protocol (MCP) connectors. The full record stays in the dataset. Leave empty to skip. Supported: Notion, Linear, Airtable, Apify.

## `notionParentPageUrl` (type: `string`):

URL (or id) of the Notion page under which item pages are created. Required to enable the Notion export; ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on items written to each connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "search",
  "queries": [
    "bmw"
  ],
  "startUrls": [
    "https://www.2dehands.be/q/bmw/",
    "https://www.2ememain.be/q/iphone/",
    "https://www.2dehands.be/q/bmw/p/2/",
    "https://www.2dehands.be/q/bmw/?sortBy=SORT_INDEX&sortOrder=DECREASING&priceFrom=100&priceTo=5000",
    "https://www.2dehands.be/q/bmw/p/2/?sortBy=SORT_INDEX&sortOrder=DECREASING",
    "https://www.2dehands.be/q/bmw/#Language:all-languages|sortBy:SORT_INDEX|sortOrder:DECREASING|offeredSince:Today",
    "https://www.2dehands.be/l/auto-s/",
    "https://www.2dehands.be/l/auto-s/p/2/",
    "https://www.2dehands.be/l/auto-s/q/bmw/",
    "https://www.2dehands.be/l/auto-s/bmw/",
    "https://www.2dehands.be/v/auto-s/bmw/m2453646187-bmw-118i",
    "https://www.2ememain.be/v/velos-velomoteurs/velos-hommes-velos-de-sport-velo-de-randonnee/m2461083552-velo-electrique-bergamont-e-revox-4",
    "https://2dehands.be/q/volvo/",
    "https://www.2dehands.be/u/geert-baart/57629670/",
    "https://www.2dehands.be/lrp/api/search?query=fiets&limit=3&offset=0"
  ],
  "domain": "2dehands.be",
  "minPrice": 0,
  "maxPrice": 0,
  "sort": "optimized",
  "categoryId": 0,
  "delivery": "any",
  "buyItNowOnly": false,
  "fetchDetails": false,
  "scrapeDetails": false,
  "includeReviews": true,
  "maxReviews": 20,
  "maxItems": 20,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "BE"
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "onlyNewListings": false,
  "resetState": false,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 50
}
```

# Actor output Schema

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

No description

## `sellers` (type: `string`):

No description

## `reviews` (type: `string`):

No description

## `changes` (type: `string`):

No description

## `details` (type: `string`):

No description

# 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 = {
    "mode": "search",
    "queries": [
        "bmw"
    ],
    "startUrls": [
        "https://www.2dehands.be/q/bmw/",
        "https://www.2ememain.be/q/iphone/",
        "https://www.2dehands.be/q/bmw/p/2/",
        "https://www.2dehands.be/q/bmw/?sortBy=SORT_INDEX&sortOrder=DECREASING&priceFrom=100&priceTo=5000",
        "https://www.2dehands.be/q/bmw/p/2/?sortBy=SORT_INDEX&sortOrder=DECREASING",
        "https://www.2dehands.be/q/bmw/#Language:all-languages|sortBy:SORT_INDEX|sortOrder:DECREASING|offeredSince:Today",
        "https://www.2dehands.be/l/auto-s/",
        "https://www.2dehands.be/l/auto-s/p/2/",
        "https://www.2dehands.be/l/auto-s/q/bmw/",
        "https://www.2dehands.be/l/auto-s/bmw/",
        "https://www.2dehands.be/v/auto-s/bmw/m2453646187-bmw-118i",
        "https://www.2ememain.be/v/velos-velomoteurs/velos-hommes-velos-de-sport-velo-de-randonnee/m2461083552-velo-electrique-bergamont-e-revox-4",
        "https://2dehands.be/q/volvo/",
        "https://www.2dehands.be/u/geert-baart/57629670/",
        "https://www.2dehands.be/lrp/api/search?query=fiets&limit=3&offset=0"
    ],
    "domain": "2dehands.be",
    "minPrice": 0,
    "maxPrice": 0,
    "sort": "optimized",
    "categoryId": 0,
    "fetchDetails": false,
    "includeReviews": true,
    "maxReviews": 20,
    "maxItems": 20,
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "BE"
    },
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "ignoreFieldsForChanges": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/2dehands-2ememain-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 = {
    "mode": "search",
    "queries": ["bmw"],
    "startUrls": [
        "https://www.2dehands.be/q/bmw/",
        "https://www.2ememain.be/q/iphone/",
        "https://www.2dehands.be/q/bmw/p/2/",
        "https://www.2dehands.be/q/bmw/?sortBy=SORT_INDEX&sortOrder=DECREASING&priceFrom=100&priceTo=5000",
        "https://www.2dehands.be/q/bmw/p/2/?sortBy=SORT_INDEX&sortOrder=DECREASING",
        "https://www.2dehands.be/q/bmw/#Language:all-languages|sortBy:SORT_INDEX|sortOrder:DECREASING|offeredSince:Today",
        "https://www.2dehands.be/l/auto-s/",
        "https://www.2dehands.be/l/auto-s/p/2/",
        "https://www.2dehands.be/l/auto-s/q/bmw/",
        "https://www.2dehands.be/l/auto-s/bmw/",
        "https://www.2dehands.be/v/auto-s/bmw/m2453646187-bmw-118i",
        "https://www.2ememain.be/v/velos-velomoteurs/velos-hommes-velos-de-sport-velo-de-randonnee/m2461083552-velo-electrique-bergamont-e-revox-4",
        "https://2dehands.be/q/volvo/",
        "https://www.2dehands.be/u/geert-baart/57629670/",
        "https://www.2dehands.be/lrp/api/search?query=fiets&limit=3&offset=0",
    ],
    "domain": "2dehands.be",
    "minPrice": 0,
    "maxPrice": 0,
    "sort": "optimized",
    "categoryId": 0,
    "fetchDetails": False,
    "includeReviews": True,
    "maxReviews": 20,
    "maxItems": 20,
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "BE",
    },
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "ignoreFieldsForChanges": [],
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/2dehands-2ememain-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 '{
  "mode": "search",
  "queries": [
    "bmw"
  ],
  "startUrls": [
    "https://www.2dehands.be/q/bmw/",
    "https://www.2ememain.be/q/iphone/",
    "https://www.2dehands.be/q/bmw/p/2/",
    "https://www.2dehands.be/q/bmw/?sortBy=SORT_INDEX&sortOrder=DECREASING&priceFrom=100&priceTo=5000",
    "https://www.2dehands.be/q/bmw/p/2/?sortBy=SORT_INDEX&sortOrder=DECREASING",
    "https://www.2dehands.be/q/bmw/#Language:all-languages|sortBy:SORT_INDEX|sortOrder:DECREASING|offeredSince:Today",
    "https://www.2dehands.be/l/auto-s/",
    "https://www.2dehands.be/l/auto-s/p/2/",
    "https://www.2dehands.be/l/auto-s/q/bmw/",
    "https://www.2dehands.be/l/auto-s/bmw/",
    "https://www.2dehands.be/v/auto-s/bmw/m2453646187-bmw-118i",
    "https://www.2ememain.be/v/velos-velomoteurs/velos-hommes-velos-de-sport-velo-de-randonnee/m2461083552-velo-electrique-bergamont-e-revox-4",
    "https://2dehands.be/q/volvo/",
    "https://www.2dehands.be/u/geert-baart/57629670/",
    "https://www.2dehands.be/lrp/api/search?query=fiets&limit=3&offset=0"
  ],
  "domain": "2dehands.be",
  "minPrice": 0,
  "maxPrice": 0,
  "sort": "optimized",
  "categoryId": 0,
  "fetchDetails": false,
  "includeReviews": true,
  "maxReviews": 20,
  "maxItems": 20,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "BE"
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": []
}' |
apify call abotapi/2dehands-2ememain-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/2dehands-2ememain-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/W1e1aRIhkVIdnrsNa/builds/oiAZ7MGhZeBAfc52H/openapi.json
