# 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
- **Stats:** 4 total users, 3 monthly users, 100.0% runs succeeded, 0 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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## 2dehands & 2ememain Scraper

Scrape structured listing data from **2dehands.be** and **2ememain.be** - Belgium's two largest general classifieds marketplaces (cars, electronics, furniture, bikes, fashion, and more). Both sites share one catalogue, so a single actor covers both.

Returns normalized listing records: item id + canonical URL, title, description, price and price type, full location (city, country, coordinates), images, seller, category, listing flags, plus - when details are on - view/favorite counts, seller profile, item attributes, shipping, bidding, and the seller's full **tag-based reviews**.

### Why use this actor

- **Resale & pricing analysts** - track asking prices, price types, and category supply across Belgium over time.
- **Marketplace intelligence** - benchmark seller activity, verification, and buyer feedback for competitor or vendor research.
- **Data teams** - feed structured records into dashboards, warehouses, alerts, or matching engines.
- **Lead generation** - surface active sellers and dealers with contact context for prospect research.

### Input parameters

Provide a keyword + filters (search mode) or paste listing/search URLs (url mode).

| Parameter | Type | Description | Default |
| --- | --- | --- | --- |
| `mode` | `string` | `search` (keyword + filters) or `url` (paste listing/search links). | `search` |
| `queries` | `string[]` | Search keywords (one per line), e.g. `bmw`, `iphone`, `fiets`. Each runs a separate search. | - |
| `domain` | `string` | `2dehands.be` (Dutch) or `2ememain.be` (French). Same catalogue. | `2dehands.be` |
| `minPrice` | `integer` | Keep only listings priced at or above this amount (EUR). `0` = keep all. | `0` |
| `maxPrice` | `integer` | Keep only listings priced at or below this amount (EUR). `0` = no upper bound. | `0` |
| `sort` | `string` | `optimized` | `date_desc` | `date_asc` | `price_asc` | `price_desc`. | `optimized` |
| `categoryId` | `integer` | Restrict to a top-level category by numeric id. `0` = all categories. | `0` |
| `startUrls` | `string[]` | Search/category pages (e.g. `https://www.2dehands.be/q/bmw/`) or single listing pages (a `/v/` link fetches just that item). | - |
| `fetchDetails` | `boolean` | Open each listing page for view/favorite counts, full category, seller profile, item attributes, shipping, bidding, and reviews. Billed as a detail-enrichment surcharge. | `false` |
| `includeReviews` | `boolean` | When details are on, capture the seller's tag-based reviews and rating summary. | `true` |
| `maxReviews` | `integer` | Cap on individual reviews captured per listing's seller. | `20` |
| `maxItems` | `integer` | **The run cap** - global max listings returned (`0` = unlimited, clamped to 1000). | `20` |
| `maxPages` | `integer` | Per-search page limit (30 listings/page). Defaults to no practical limit (stops at `maxItems`). | `200` |
| `proxy` | `object` | Proxy settings. Residential country BE is recommended. | Residential BE |
| `resumeFromRunId` | `string` | Continue one specific previous run/dataset: listings already saved there are loaded before scraping starts and skipped, so this run only appends new listings. For recurring monitoring of the same search, use `incrementalMode` instead — see "Resume & recurring updates" below. | (empty) |
| `incrementalMode` | `boolean` | Recurring monitoring of this same search. First run returns everything as `NEW`; later runs return only `NEW`/`UPDATED`/`REAPPEARED` by default. See "Resume & recurring updates" below. | `false` |
| `stateKey` | `string` | Optional name for a monitoring campaign, so its incremental state stays stable or is deliberately shared. Auto-derived from your search/filter settings when left empty. | (empty) |
| `emitUnchanged` | `boolean` | Incremental mode only. Also return listings unchanged since the last run, marked `UNCHANGED`. Adds and bills extra rows you already have. | `false` |
| `emitExpired` | `boolean` | Incremental mode only. Also return listings from a previous run no longer found, marked `EXPIRED`, once a run has fully scanned the search (not capped, not a resume). Adds and bills extra synthetic rows. | `false` |

#### One cap rule

`maxItems` is the single cap. `maxPages` defaults high and stops at `maxItems`. Set `maxPages` only when you want a hard per-search page limit.

#### Price filter

`minPrice` / `maxPrice` are applied to each listing's price so the result is always exact. Note that some listings use non-fixed price types (see below) with a zero price; those are excluded whenever a price bound is set.

### Output

Each record carries a stable `type` + `itemId` + `url` envelope. Core and extended fields:

- **Core**: `itemId`, `title`, `url`, `description`, `categorySpecificDescription`, `thinContent`, `sourceSite`, `seedValue`, `scrapedAt`
- **Incremental mode only**: `changeType` (`NEW`/`UPDATED`/`UNCHANGED`/`REAPPEARED`/`EXPIRED`), `changedFields`, `firstSeenAt`, `lastSeenAt` — see "Resume & recurring updates" below
- **Price**: `price`, `priceCents`, `priceType` (e.g. `FIXED`, `RESERVED`, `MIN_BID`, `FAST_BID`, `ON_REQUEST`, `FREE`, `EXCHANGE`, `SEE_DESCRIPTION`)
- **Location**: `city`, `countryName`, `countryAbbreviation`, `latitude`, `longitude`, `distanceMeters`
- **Media**: `imageUrl`, `imageUrls[]`, `pictures[]` (raw, with all size variants), `date`
- **Seller (card)**: `sellerId`, `sellerName`, `sellerVerified`, `sellerInformation` (raw)
- **Flags**: `categoryId`, `priorityProduct`, `videoOnVip`, `urgencyFeatureActive`, `napAvailable`, `reserved`, `traits[]`, `verticals[]`, `extendedAttributes[]`

When `fetchDetails` is on, each record is also enriched with:

- **Listing detail**: `viewCount`, `favoritedCount`, `detailImageUrls[]`, `categoryName`, `categoryFullName`, `categoryParentName`, `attributes[]` (plus flattened `attr_<key>`), `carDetails` (for vehicles), `shippingDeliveryType`, `biddingEnabled`, `bidCount`, `adType`, `buyItNowEnabled`, `isReserved`, `highlights[]`
- **Seller profile**: `sellerType` (consumer/dealer), `sellerActiveYears`, `sellerProfileUrl`, `sellerContactOptions[]`, `sellerPaymentMethod`, `sellerSmbVerified`, `sellerHasPhone`, `sellerResponseRate`, `sellerResponseSpeed`, `sellerResponseLabel`
- **Reviews**: `sellerRating`, `sellerReviewCount`, `sellerReviewSystem`, `sellerReviewsSummary` (average score, rating distribution, positive/negative characteristic tags), and `reviews[]`

#### Reviews are tag-based (captured in full)

Seller feedback on these sites is **structured, not free-text prose**. Each review carries a 1-5 score, the transacted item's title and category, the reviewer's name, the date, the review direction (buyer-to-seller or seller-to-buyer), and a set of positive/negative **characteristic tags** (short phrases). Every one of those fields is captured. Each `reviews[]` element looks like:

```json
{
  "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 }
  ]
}
```

### Example inputs

Keyword search:

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

Listings with full detail + reviews:

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

Paste URLs:

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

### Resume & recurring updates

| Situation | Use |
| --- | --- |
| A crawl stopped and should continue | `resumeFromRunId` |
| Run the same search on a schedule and receive only changes | `incrementalMode` |
| Keep separate recurring campaigns for similar searches | distinct `stateKey` values |

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

**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 as `NEW`. Later runs return only `NEW`, `UPDATED`, and `REAPPEARED` listings by default — duplicates and unchanged listings are suppressed (and not charged). Turn on `emitUnchanged` or `emitExpired` only when you also want those rows returned (and billed for). State is isolated per mode/query/domain/category/price-filter/URL/`fetchDetails` combination automatically; set `stateKey` to name or deliberately share a monitoring campaign.

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

`EXPIRED` rows are only produced once a run has genuinely scanned every page of every search with no cap hit and no `resumeFromRunId` — a capped or resumed run leaves previously-tracked listings' state untouched rather than guessing they disappeared.

### 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 - Notion, Linear, Airtable, or Apify. Select connectors in the input; the full record always stays in the dataset. For the Notion connector, provide a parent page. Connections go through the Apify MCP Proxy, so the actor never sees your third-party credentials.

### Pricing

Pay-per-event: a `Results` event per dataset row, plus a small `detail-enrichment` event per listing enriched (only when `fetchDetails` is on). An Apify `Actor Start` event covers compute.

### Connection guidance

Apify **Residential** proxy set to country **BE** gives the highest reliability, since both sites are Belgian. Some datacenter exit IPs may be geo-restricted; if a run returns no results, switch the proxy to Residential country BE or re-run for a fresh exit IP. If you are on the Apify Free plan, residential proxy access is not included - upgrade or paste your own residential proxy URLs in the proxy field.

### Compliance & ethics

This actor collects publicly available classifieds information from 2dehands.be and 2ememain.be for legitimate research purposes (pricing research, marketplace monitoring, competitive analysis). Users are responsible for ensuring their collection, storage, and use complies with applicable laws, regulations, and platform terms. This section is informational, not legal advice.

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

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

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

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

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

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

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

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

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

One or more URLs. Accepts search or category pages (e.g. https://www.2dehands.be/q/bmw/) and single listing pages (a /v/ link fetches just that one item). Multi-URL supported. Filters above are ignored in this mode; pagination starts at the page implied by the URL and walks forward.

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

## `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). Defaults to no practical limit - the run stops at Max listings.

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

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

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

## Actor input object example

```json
{
  "mode": "search",
  "queries": [
    "bmw"
  ],
  "domain": "2dehands.be",
  "minPrice": 0,
  "maxPrice": 0,
  "sort": "optimized",
  "categoryId": 0,
  "startUrls": [
    "https://www.2dehands.be/q/bmw/"
  ],
  "fetchDetails": false,
  "includeReviews": true,
  "maxReviews": 20,
  "maxItems": 20,
  "maxPages": 200,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "BE"
  },
  "maxNotifyListings": 50,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false
}
```

# Actor output Schema

## `results` (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"
    ],
    "domain": "2dehands.be",
    "minPrice": 0,
    "maxPrice": 0,
    "sort": "optimized",
    "categoryId": 0,
    "startUrls": [
        "https://www.2dehands.be/q/bmw/"
    ],
    "fetchDetails": false,
    "includeReviews": true,
    "maxReviews": 20,
    "maxItems": 20,
    "maxPages": 200,
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "BE"
    },
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false
};

// 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"],
    "domain": "2dehands.be",
    "minPrice": 0,
    "maxPrice": 0,
    "sort": "optimized",
    "categoryId": 0,
    "startUrls": ["https://www.2dehands.be/q/bmw/"],
    "fetchDetails": False,
    "includeReviews": True,
    "maxReviews": 20,
    "maxItems": 20,
    "maxPages": 200,
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "BE",
    },
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
}

# 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("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).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"
  ],
  "domain": "2dehands.be",
  "minPrice": 0,
  "maxPrice": 0,
  "sort": "optimized",
  "categoryId": 0,
  "startUrls": [
    "https://www.2dehands.be/q/bmw/"
  ],
  "fetchDetails": false,
  "includeReviews": true,
  "maxReviews": 20,
  "maxItems": 20,
  "maxPages": 200,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "BE"
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false
}' |
apify call abotapi/2dehands-2ememain-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=abotapi/2dehands-2ememain-scraper",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/W1e1aRIhkVIdnrsNa/builds/Cvg55OwhieYWA03xG/openapi.json
