# bol.com Scraper - Products, Prices, Offers, Reviews (`abotapi/bol-com-scraper`) Actor

Scrape bol.com products: title, price, list price and discount, EAN, brand, images, condition, delivery, full specifications, ratings with distribution, individual reviews, every offer with seller name and rating, and refurbished prices. Search by keyword with filters, or paste product URLs.

- **URL**: https://apify.com/abotapi/bol-com-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** E-commerce, Automation, Developer tools
- **Features:** Limited permissions
- **Stats:** 134 total users, 41 monthly users, 94.5% runs succeeded, 2 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $1.00 / 1,000 product scrapeds

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

## Bol.com Scraper: Products, Prices, Offers and Reviews

Bol.com Scraper turns Bol.com, the largest online store in the Netherlands and Belgium, into a clean product data feed. Get price, list price and discount, brand, EAN, full specifications, images, condition, delivery estimate, rating with the full review distribution, individual reviews, every seller offer, and refurbished or second-hand pricing. Search by keyword with price, rating, condition and sort filters, or paste product and result-page links, then export to JSON, CSV or Excel, or pull the results straight into your app through the API.

### Why This Scraper?

- **One record, most of what you need.** Every product carries price, list price, discount, currency, brand, category path, condition, delivery estimate, rating summary, image and seller without turning anything on.
- **Full detail on demand.** Turn on Fetch full product details to add the complete specification list (including EAN), the full description, every product image, the full review list with star distribution, seller rating, product weight, and refurbished or second-hand offers.
- **Six filters before anything is collected.** Price band, minimum rating, condition, sponsored-only and in-stock-only narrow results before they are scraped or charged, on top of six sort orders.
- **Two ways in.** Search mode builds queries from keywords, or paste product and search-result URLs directly in URL mode.
- **Bol.com's own deal pages.** An optional specials source adds the storewide deals page or the clearance page alongside your keywords, and each product carries real was-price, savings and promo-label fields straight from that listing.
- **One finite cap.** Max items bounds a run's size and cost; page depth is unlimited unless you set it.
- **Built for schedules.** Incremental mode returns only new, updated and reappeared products on repeat runs, and Resume continues one interrupted crawl without re-collecting or re-paying for what it already has.

### Use Cases

- **Price trackers and deal hunters:** watch prices, discounts and Bol.com's own specials pages on a recurring schedule.
- **E-commerce and marketplace analysts:** compare pricing, ratings and seller performance across categories.
- **Retail catalog teams:** pull full specifications, EAN codes and every product image to enrich a catalog.
- **Review and reputation research:** collect the full review list and star distribution per product.
- **Resellers and price-comparison tools:** track refurbished and second-hand offers alongside new-condition pricing.

### Data You Get

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

| Field | Example |
| --- | --- |
| `productId` | `"9300000012345"` |
| `title` | `"Voorbeeld Draadloze Hoofdtelefoon met Noise Cancelling, Zwart"` |
| `brand` | `"Voorbeeldmerk"` |
| `price` / `currency` | `279.0` / `"EUR"` |
| `list_price` / `discount_percentage` | `349.99` / `20` |
| `condition` | `"new"` |
| `ean` | `"0000000000000"` |
| `category` / `categoryHierarchy` | `"Koptelefoons"` / `["Elektronica", "Audio", "Koptelefoons"]` |
| `rating` / `reviewCount` | `4.6` / `812` |
| `ratingDistribution` | `{"fiveStar": 600, "fourStar": 150, "threeStar": 40, "twoStar": 12, "oneStar": 10}` |
| `image` / `images` | product photo URL(s) |
| `availability` / `delivery` | `"Voor 23:59 uur besteld, morgen in huis"` (both carry the delivery estimate) |
| `seller` | `{"id": "0000000", "name": "Voorbeeldverkoper", "rating": 9.2, "topSeller": true}` |
| `weight` | `"254 g"` (when full details are on) |
| `sponsored` | `false` |
| `offers` | every selling offer: price, condition and seller (when full details are on) |
| `specifications` | full spec list, group and name/value pairs incl. EAN (when full details are on) |
| `reviews` | individual reviews with rating, text, nickname and verified flag (when full details are on) |
| `isOnSpecial` / `savingsAmount` / `savingsPercent` / `promoLabel` / `specialsCategory` | Bol.com's own was-price, savings and promo-badge fields |
| `has_refurbished` / `in_stock` | `false` / `true` |
| `url` | link to the product page |

In incremental mode, every returned record also carries `changeType`, `changedFields`, `firstSeenAt` and `lastSeenAt`; these four fields are absent from a normal run.

### How to Use

1. Pick a **mode**: `search` (keyword and filters) or `url` (paste product or result-page links).
2. In search mode, add one or more keywords and, optionally, narrow with the price, rating, condition, sponsored and in-stock filters and a sort order. In URL mode, paste Bol.com product or result-page links; the same filters still apply.
3. Turn on **Fetch full product details** to add specifications, EAN, full reviews, seller rating, weight and refurbished offers, and set **Max items** to bound the run.
4. Click **Start**, then download the dataset as JSON, CSV or Excel, or read it through the API.

**Search by keyword with a price ceiling:**

```json
{
  "mode": "search",
  "searchTerms": ["laptop"],
  "sortBy": "price_asc",
  "maxPrice": 400,
  "maxListings": 50
}
```

**Search with full product detail enrichment:**

```json
{
  "mode": "search",
  "searchTerms": ["koffiezetapparaat"],
  "fetchDetails": true,
  "maxReviews": 20,
  "minRating": 4,
  "maxListings": 30
}
```

**Bol.com's own deals page as an extra source:**

```json
{
  "mode": "search",
  "searchTerms": [],
  "specialsCategory": "aanbiedingen",
  "maxListings": 50
}
```

**Scrape pasted URLs (product pages and result pages):**

```json
{
  "mode": "url",
  "urls": [
    "https://www.bol.com/nl/nl/p/example-laptop/9300000000000/",
    "https://www.bol.com/nl/nl/s/?searchtext=boek"
  ],
  "maxListings": 100
}
```

#### Supported URL shapes

In URL mode, leaving `urls` untouched now scrapes one example of each shape below (earlier versions scraped nothing). Set your own list to scrape only what you need.

| URL shape | Example | Result |
|---|---|---|
| Keyword search | `https://www.bol.com/nl/nl/s/?searchtext=laptop` | listed products |
| Search, page N | `.../s/?searchtext=laptop&page=2` | starts at that page (never marks products `EXPIRED`) |
| Search, sorted | `.../s/?searchtext=laptop&sort=price&sortOrder=asc&page=2` | sort read from the URL |
| Search slug | `https://www.bol.com/nl/nl/s/laptop/` | keyword taken from the slug |
| Search data link | `https://www.bol.com/nl/nl/s/.data?searchtext=laptop&page=1` | same as keyword search |
| Product | `https://www.bol.com/nl/nl/p/<slug>/<id>/` | full detail record, matched on the numeric id |
| Product with tracking parameters | `.../<id>/?bltgh=abc&promo=x` | same as product |
| Category | `https://www.bol.com/nl/nl/l/laptops/4770/` | listed products |
| Brand | `https://www.bol.com/nl/nl/b/acemagic/607284936/` | listed products |
| Deals | `https://www.bol.com/nl/nl/l/alle-artikelen-aanbiedingen/58334/` | listed products |
| Outlet | `https://www.bol.com/nl/nl/ra/outlet/392102/` (also `?page=2`) | listed products |
| Older search link | `https://www.bol.com/nl/s/zoekresultaten.html?searchtext=laptop` | keyword read from the link |
| Mobile host | `https://m.bol.com/nl/nl/s/?searchtext=laptop` | treated as www.bol.com |

Any other bol.com link is skipped with a warning in the log.

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/bol-com-scraper").call(run_input={"mode": "search", "searchTerms": ["laptop"], "maxListings": 20})
for product in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(product["title"], product["price"])
```

JavaScript:

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/bol-com-scraper').call({ mode: 'search', searchTerms: ['laptop'], maxListings: 20 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

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

#### Bol.com's specials pages and was-price fields

Set `specialsCategory` to `aanbiedingen` (Bol.com's storewide deals page) or `outlet` (its clearance page) to add that page as an extra product source alongside, or instead of, your search keywords. Every product, whichever source it came from, carries `isOnSpecial`, `originalPrice`, `savingsAmount`, `savingsPercent` and `promoLabel`: these are only ever set from Bol.com's own reference price and promo badge on the listing, never estimated. `originalPrice`, `savingsAmount` and `savingsPercent` are `null` when there is no real reduction, `promoLabel` is `null` when there is no badge, and `isOnSpecial` is `true` when either is present. They never change or gate any other field, and they add no extra cost since the data comes from the same listing page the actor already fetches.

#### Resume and recurring updates

- **Resume** (`resumeFromRunId`) continues one specific interrupted run: paste its run or dataset ID and this run skips products already collected there, so you don't pay twice.
- **Incremental mode** (`incrementalMode`) is for scheduled runs of the same search. The actor remembers the previous run itself, so you never paste an ID. The first run returns everything as `NEW`; later runs return only `NEW`, `UPDATED` (with `changedFields`) and `REAPPEARED` products by default, while unchanged products are suppressed and not billed unless `emitUnchanged` is on. `EXPIRED` products (previously tracked, no longer found) are only produced after a run that scanned the whole search, and only with `emitExpired` on. `stateKey` names or shares one monitoring campaign; leave it empty and the actor derives a key from the search and filter setup automatically.
- **What counts as a change.** A product is `UPDATED` when a tracked field moves: price, list price, discount, availability and delivery, condition, title, offers, seller, specifications, description or categories. Reviews and every rating or review counter, the rotating image host, URL tracking parameters and the order of offers never make a product `UPDATED`; they stay in the output. Add more field names to `ignoreFieldsForChanges` (title: "Ignore fields when detecting changes (incremental mode only)") to exclude them too. Changing that list does not trigger a one-time wave of `UPDATED` rows.
- `EXPIRED` is never produced from a partial scope: a pasted result URL that starts at `?page=2` or later, a run stopped by Max items or Max pages, or a failed page.
- Turning on Incremental mode together with Resume is only allowed to bootstrap a search's very first incremental baseline. Once that search already has saved incremental state, combining the two stops the run with a message asking you to drop Resume or pick a different `stateKey`.

#### Output views

The dataset has four views in the Apify Console: **Products** (core listing fields), **Product details** (specifications, EAN, offers, images; filled when Fetch full product details is on), **Reviews** (one row per review, unwound from each product) and **Changes (incremental)** (change type, changed fields and timestamps).

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

Optionally pipe the scraped results 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 product, 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 **"Export to your apps"** input field. (If the picker is empty, you haven't authorized a connector yet.)
3. For **Notion**, also set `notionParentPageUrl` 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 and filters) or `url` (paste product or result URLs). |
| `searchTerms` | array | `[]` | Keywords to search. Search mode only. |
| `specialsCategory` | string | `""` | Optional: add `aanbiedingen` (storewide deals page) or `outlet` (clearance page) as an extra source alongside your search keywords. Off by default. |
| `sortBy` | string | `relevance` | `relevance`, `popularity`, `price_asc`, `price_desc`, `rating`, or `newest`. Applies in both search and URL mode. |
| `minPrice` | integer | (none) | Keep products at or above this price (EUR). |
| `maxPrice` | integer | (none) | Keep products at or below this price (EUR). |
| `minRating` | integer | (none) | Keep products with an average rating at or above this value (1-5). |
| `condition` | string | `any` | `any`, `new`, `secondhand`, or `refurbished`. Keeps only products whose best offer matches. |
| `sponsoredOnly` | boolean | `false` | Keep only sponsored (advertised) products. |
| `inStockOnly` | boolean | `false` | Keep only products with a buyable offer. |
| `productIds` | array | `[]` | Look up specific products directly: bol.com product ids (16 digits), EAN/GTIN barcodes (8, 12, 13 or 14 digits), or product page URLs. Works in either mode and alongside keywords; each entry returns one product. A bol.com id opens that exact product page and returns nothing if it no longer exists; an EAN returns the top search hit for that barcode. A bol.com id or product URL always returns full detail fields; the detail enrichment event is billed for it only when Fetch full product details is on. |
| `includeSponsored` | boolean | `true` | Keep sponsored (advertised) listings. Turn off to drop them. Ignored when `sponsoredOnly` is on. |
| `urls` | array | one example per shape (see below) | Product, search, category, brand, deals or outlet URLs to scrape. Belgian search links (`/be/nl/s/`, `/be/fr/s/`) are scraped on the Belgian storefront with `country` BE; Belgian category, brand and outlet links are skipped with a warning. URL mode only; the price, rating, condition, sponsored and in-stock filters still apply. A product link always returns full detail fields; the detail enrichment event is billed for it only when Fetch full product details is on. |
| `fetchDetails` | boolean | `false` | Adds full specifications (incl. EAN), full description, every image, full reviews with rating distribution, seller rating, weight, and refurbished/second-hand offers. |
| `maxReviews` | integer | `30` | Maximum individual reviews captured per product when full details are on (maximum `1000`). Bol.com's product page serves only its first slice of reviews (about 6 per product in testing; a product with 11 reviews returned 6) and exposes no way to page further, so fewer reviews than the cap may come back. |
| `maxPages` | integer | (none) | Safety bound on result pages walked per keyword. Empty or `0` is unlimited; this does not cap the number of products. |
| `totalPages` | integer | (none) | Same as `maxPages`, kept for compatibility with other bol.com scrapers. `maxPages` wins when both are set. |
| `maxListings` | integer | `20` | Maximum products for the whole run. The single finite cap. `0` is unlimited. |
| `resumeFromRunId` | string | (none) | Continue one previous interrupted run or crawl, skipping products it already collected. |
| `incrementalMode` | boolean | `false` | Turn on for daily or recurring monitoring: later runs return only new, updated and reappeared products. |
| `stateKey` | string | (none) | Name or share an incremental-mode monitoring campaign. Auto-derived from the search and filter setup when left empty. |
| `emitUnchanged` | boolean | `false` | Incremental mode only. Also return, and bill for, products unchanged since the last run. |
| `emitExpired` | boolean | `false` | Incremental mode only. Also return, and bill for, products no longer found after a complete scan. |
| `ignoreFieldsForChanges` | array | `[]` | Incremental mode only. Top-level field names that never make a product UPDATED or appear in `changedFields` (still returned in the output). |
| `proxy` | object | `{"useApifyProxy": true}` | Proxy configuration. Keep Apify Proxy enabled with a Netherlands exit for the most complete results. |
| `residentialCountries` | array | `[]` | Preferred exit countries. Falls back to Netherlands, then Belgium, Germany, France and the United Kingdom when left empty. |
| `backupProxyUrl` | string | (none) | Optional backup proxy gateway used when full Apify Proxy access is unavailable on your plan. |
| `maxResidentialRequests` | integer | (none) | Cost guard limiting how many requests a run may make before switching to the backup gateway. `0` is unlimited; falls back to 5000 when left empty. |
| `trafficBudgetMb` | integer | (none) | Soft transfer budget in MB. When exceeded, the run switches to the backup gateway. `0` is unlimited; falls back to 1500 when left empty. |
| `preferDatacenter` | boolean | `false` | No effect: Bol.com refuses datacenter connections, so the actor always uses a residential connection. Kept so saved tasks keep working. |
| `autoDowngradeProxy` | boolean | `true` | No effect: Bol.com refuses datacenter connections, so the actor always uses a residential connection. Kept so saved tasks keep working. |
| `mcpConnectors` | array | `[]` | Optional connected apps (Notion, Linear, Airtable, Apify) to also receive each product. |
| `notionParentPageUrl` | string | (none) | When a Notion connector is selected, create one page per product under this parent page. |
| `maxNotifyListings` | integer | (none) | Optional cap on how many products are pushed to the selected connectors. Falls back to 50 when left empty. |

### Output Example

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

```json
{
  "productId": "9300000012345",
  "title": "Voorbeeld Draadloze Hoofdtelefoon met Noise Cancelling, Zwart",
  "brand": "Voorbeeldmerk",
  "price": 279.0,
  "list_price": 349.99,
  "discount_percentage": 20,
  "currency": "EUR",
  "condition": "new",
  "ean": "0000000000000",
  "category": "Koptelefoons",
  "categoryHierarchy": ["Elektronica", "Audio", "Koptelefoons"],
  "rating": 4.6,
  "reviewCount": 812,
  "ratingDistribution": {"fiveStar": 600, "fourStar": 150, "threeStar": 40, "twoStar": 12, "oneStar": 10},
  "reviews": [
    {"rating": 5, "title": "Prima aankoop", "text": "Snel geleverd en werkt goed.", "nickname": "Klant2024", "location": "Nederland", "verified": true}
  ],
  "image": "https://media.s-bol.com/0000/image-550x461.jpg",
  "images": ["https://media.s-bol.com/0000/image-550x461.jpg"],
  "availability": "Voor 23:59 uur besteld, morgen in huis",
  "delivery": "Voor 23:59 uur besteld, morgen in huis",
  "offers": [
    {"price": 279.0, "currency": "EUR", "condition": "new", "seller": {"id": "0000000", "name": "Voorbeeldverkoper", "rating": 9.2, "topSeller": true}}
  ],
  "seller": {"id": "0000000", "name": "Voorbeeldverkoper", "rating": 9.2, "topSeller": true},
  "specifications": [
    {"group": "Algemeen", "name": "EAN", "value": "0000000000000"},
    {"group": "Algemeen", "name": "Kleur", "value": "Zwart"}
  ],
  "description": "Voorbeeldbeschrijving van dit product voor studie, werk of thuisgebruik.",
  "weight": "254 g",
  "weightGrams": 254,
  "sponsored": false,
  "has_refurbished": false,
  "in_stock": true,
  "isOnSpecial": true,
  "originalPrice": 349.99,
  "savingsAmount": 70.99,
  "savingsPercent": 20,
  "promoLabel": "deal",
  "specialsCategory": null,
  "products_found": 4210,
  "search_query": "koptelefoon",
  "country": "NL",
  "url": "https://www.bol.com/nl/nl/p/voorbeeld-hoofdtelefoon/9300000012345/",
  "scrapedAt": "2026-01-01T00:00:00+00:00"
}
```

### Plan Requirement

**Residential proxies are required.** Bol.com refuses datacenter connections outright: in testing, Apify datacenter proxies (automatic and datacenter groups) and a plain home connection all received an HTTP 403 "access blocked" page, while a Netherlands residential exit returned the full results. Keep Apify Proxy enabled and leave the connection on its default. A selected datacenter group is replaced by residential (billed to your plan), because Bol.com refuses datacenter connections; the actor uses a Netherlands residential exit first, then Belgium, Germany, France and the United Kingdom (change the order with `residentialCountries`). Residential exits are also uneven: an occasional exit is refused for a valid page, and the actor simply rolls to a fresh exit and retries.

Apify free plans do not include residential proxies. On a free plan the run logs this warning and continues on the backup connection (`backupProxyUrl`, or the fallback gateway), where some or all pages may stay unavailable:

> The selected connection is unavailable to this run. Apify free plans do not include residential proxies. Continuing on a backup connection; some pages may still be unavailable. For residential access, upgrade at https://apify.com/pricing or supply your own proxy URLs.

When pages do load through the backup, the log says: "Served over the backup connection (the default connection was unavailable)." If nothing could be read, the run ends with one of these messages instead of a silent empty result:

- **Proxy refused for the account:** "The proxy connection was refused for this account, so no page could be read. Your Apify plan does not include proxy access; a paid plan is required: https://apify.com/pricing. If you entered your own proxy URLs, check that their credentials are valid."
- **Residential unavailable, backup could not load pages:** "No results: residential proxies are unavailable on this run (Apify free plans do not include them), and the backup connection could not load the requested bol.com pages. Upgrade at https://apify.com/pricing or supply your own proxy URLs, then try again."
- **Requests failed:** "Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search. Please try again later."

`preferDatacenter` and `autoDowngradeProxy` have no effect, because Bol.com refuses datacenter connections. `maxResidentialRequests` and `trafficBudgetMb` bound how much a run uses before it moves to the backup gateway.

### FAQ

#### How much does it cost?

Pay-per-event pricing; the current prices for every plan are on the Pricing tab. The events are:

| Event | When |
| --- | --- |
| Actor Start | once per run |
| Product scraped | once per product returned to the dataset |
| Product detail enrichment | once per product pushed with its detail page fetched, only when Fetch full product details is on (reviews included); pasted product URLs and bol.com ids in Product IDs always read the detail page for full fields, and with Fetch full product details off they are billed only as Product scraped |

Reviews ride the detail enrichment event; there is no separate review charge. Max items caps how large any run can get.

#### Is it legal to scrape Bol.com?

This actor collects only publicly available product listings. You are responsible for how you use the data: follow Bol.com's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Prices and specifications are generally factual, but images, descriptions and reviews may be subject to third-party rights.

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

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental mode**. Each run then returns only new, updated and reappeared products, and unchanged ones are not billed unless **Emit unchanged** is on.

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

This is the most common mix-up. **Resume** (`resumeFromRunId`) continues one specific interrupted run by skipping products it already collected. **Incremental mode** remembers the previous run of the *same* search by itself, for recurring scheduled runs, so you never paste an ID. The two can be combined only to bootstrap a search's very first incremental baseline from a resumed dataset; combining them again once that search already has saved incremental state stops the run with a message telling you to drop Resume or set a different `stateKey`.

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

A run fails when nothing could be read because of the connection: the proxy was refused for your account, residential proxies are unavailable and the backup connection could not load pages, or every request failed. The failure message is one of the three quoted under **Plan Requirement**, and nothing is charged for pages that never loaded. A run also fails when `resumeFromRunId` points to a run or dataset it cannot read, or when Incremental mode is combined with Resume while that search already has saved incremental state. A keyword that genuinely matches nothing still completes with an empty dataset and a log note.

#### 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 also push each product to 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/vinted-fr-scraper"><b>Vinted.fr</b></a><br>Scrape Vinted.fr, France's C2C second-hand fashion marketplace: search with...</td><td>🏷️ <a href="https://apify.com/abotapi/chrono24-scraper"><b>Chrono24 Scraper</b></a><br>Scrape Chrono24 luxury watch listings by keyword, filters or URL. Extract 50+ fields...</td></tr>
<tr><td>🏷️ <a href="https://apify.com/abotapi/daangn-scraper"><b>Daangn Scraper</b></a><br>Scrape Daangn (당근) marketplace listings by keyword and region. Returns 35+ fields per...</td><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></tr>
<tr><td>🏷️ <a href="https://apify.com/abotapi/2dehands-2ememain-scraper"><b>2dehands &amp; 2ememain</b></a><br>Scrape classifieds from 2dehands.be and 2ememain.be by keyword, filters, or URLs. Returns...</td><td>🏷️ <a href="https://apify.com/abotapi/wildberries-marketplace-scraper"><b>Wildberries Scraper</b></a><br>Scrape Wildberries marketplace: product search with prices (wallet vs retail), discounts...</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/bol-com-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`):

Choose Search to build queries from keywords and filters, or URL to scrape pasted product or result-page URLs directly.

## `searchTerms` (type: `array`):

One or more keywords to search for, e.g. "laptop", "koffiezetapparaat".

## `productIds` (type: `array`):

Look up specific products directly: bol.com product ids (16 digits, e.g. 9300000230842700), EAN/GTIN barcodes (8, 12, 13 or 14 digits), or product page URLs. Works in either mode and alongside search keywords; each entry returns one product. A bol.com id opens that exact product page (nothing is returned if it no longer exists); EAN/GTIN lookups return the top bol.com search hit for that barcode. A bol.com id or product URL always returns full detail fields; the detail enrichment charge applies to it only when Fetch full product details is on.

## `specialsCategory` (type: `string`):

Optional. Add one of bol.com's own promo/discount browse pages as an extra source for this run, alongside your search keywords. Each returned product carries real was-price/discount fields (isOnSpecial, savingsAmount, savingsPercent, promoLabel) straight from bol.com's own listing data. Off by default.

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

Result ordering. Applies in both Search and URL mode: for a pasted search-result URL, this input's sort is applied when the search is rebuilt, and the pasted URL's own sort order (if any) is not read or preserved.

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

Only keep products at or above this price.

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

Only keep products at or below this price.

## `minRating` (type: `integer`):

Only keep products with an average review rating at or above this value (1-5).

## `condition` (type: `string`):

Keep only products whose best offer matches this condition.

## `sponsoredOnly` (type: `boolean`):

Keep only sponsored (advertised) products.

## `includeSponsored` (type: `boolean`):

Keep sponsored (advertised) listings in the results. Turn off to drop them. Ignored when Sponsored only is on.

## `inStockOnly` (type: `boolean`):

Keep only products that have a buyable offer.

## `urls` (type: `array`):

Paste bol.com product, search-result, category (/l/), brand (/b/), deals or outlet (/ra/) URLs. Belgian search links (/be/nl/s/ and /be/fr/s/) are scraped on the Belgian storefront and records carry country BE; Belgian category, brand and outlet links are skipped. The price, rating, condition, sponsored and in-stock filters still apply. A product link always returns full detail fields; the detail enrichment charge applies to it only when Fetch full product details is on. Left untouched, one example of each shape runs.

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

Adds the full specifications (incl. EAN), full description, every image, the full review list with rating distribution, seller rating, weight, and refurbished/second-hand offers. Without it, runs already return price, image, rating summary, condition and seller.

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

Maximum number of individual reviews captured per product when Fetch full product details is on. Bol.com's product page serves only its first slice of reviews (about 6 per product in testing) and exposes no way to page further, so fewer reviews than the cap may come back.

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

Safety bound on result pages walked per keyword. Leave empty or 0 for unlimited. This does NOT cap the number of products; the run still stops at Max items.

## `totalPages` (type: `integer`):

Same as Max pages per search, kept for compatibility with other bol.com scrapers. Max pages wins when both are set.

## `maxListings` (type: `integer`):

Maximum number of products to return across the whole run. The single finite cap. Set 0 for unlimited.

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

Keep Apify Proxy enabled with a Netherlands exit for the most complete bol.com results. A selected datacenter group is replaced by residential (billed to your plan), because Bol.com refuses datacenter connections.

## `residentialCountries` (type: `array`):

Preferred exit countries. Defaults to NL then BE, DE, FR, GB.

## `backupProxyUrl` (type: `string`):

Optional backup proxy gateway used when full Apify Proxy access is unavailable on your plan.

## `maxResidentialRequests` (type: `integer`):

Cost guard limiting how many requests the run may make before switching to the backup gateway. 0 = unlimited. Default 5000.

## `trafficBudgetMb` (type: `integer`):

Soft transfer budget. When exceeded, the actor switches to the backup gateway. 0 = unlimited. Default 1500.

## `preferDatacenter` (type: `boolean`):

No effect: Bol.com refuses datacenter connections, so the actor always uses a residential connection. Kept so saved tasks keep working.

## `autoDowngradeProxy` (type: `boolean`):

No effect: Bol.com refuses datacenter connections, so the actor always uses a residential connection. Kept so saved tasks keep working.

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

Paste a previous run ID or dataset ID to continue a large crawl of products without returning or charging for products already collected there. Use this after an interrupted run, or when continuing a product catalogue pull in another run. 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 products as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED products. A product is UPDATED when a tracked field changes: price, list price, discount, availability and delivery, condition, title, offers, seller, specifications, description or categories. These never make a product UPDATED (they stay in the output): reviews and every rating or review counter, the rotating image host, URL tracking parameters, and the order of offers. Use "Ignore fields when detecting changes" below to exclude more fields. Turn on "Emit unchanged" or "Emit expired" only when you also want those products returned (and billed). State is kept separately for each search/URL/specials/detail-mode setup; 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 search/URL/specials and filter settings — different searches then never mix state with each other.

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

Off by default. Turn on to also return products 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 products 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 items capped it or when Resume was used. This returns — and bills — extra synthetic rows, so leave it off unless you need expiry tracking.

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

Optional. Top-level output field names (for example `description` or `delivery`) that must never make a product UPDATED or appear in `changedFields`. They are still returned in the output. Added on top of the built-in exclusions (reviews, rating counters). Applies to the saved state too, so changing this list does not cause a one-time wave of UPDATED rows.

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

Optional. Pick connected apps (Notion, Airtable, Linear, Apify) to also push each scraped product to, in addition to the dataset.

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

Optional. When a Notion connector is selected, create one page per product under this parent page.

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

Optional cap on how many products are pushed to the selected MCP connectors.

## Actor input object example

```json
{
  "mode": "search",
  "searchTerms": [
    "laptop"
  ],
  "productIds": [],
  "specialsCategory": "",
  "sortBy": "relevance",
  "condition": "any",
  "sponsoredOnly": false,
  "includeSponsored": true,
  "inStockOnly": false,
  "urls": [
    "https://www.bol.com/nl/nl/s/?searchtext=laptop",
    "https://www.bol.com/nl/nl/s/?searchtext=laptop&page=2",
    "https://www.bol.com/nl/nl/s/?searchtext=laptop&sort=price&sortOrder=asc&page=2",
    "https://www.bol.com/nl/nl/s/laptop/",
    "https://www.bol.com/nl/nl/s/.data?searchtext=laptop&page=1",
    "https://www.bol.com/nl/nl/p/acer-chromebook-cb314-4h-c4hf-intel-n100-14-full-hd-8-gb-lpddr5-128-gb-emmc/9300000263156289/",
    "https://www.bol.com/nl/nl/p/acer-chromebook-cb314-4h-c4hf-intel-n100-14-full-hd-8-gb-lpddr5-128-gb-emmc/9300000263156289/?bltgh=abc&promo=x",
    "https://www.bol.com/nl/nl/l/laptops/4770/",
    "https://www.bol.com/nl/nl/l/action-camera-s/4785/",
    "https://www.bol.com/nl/nl/b/acemagic/607284936/",
    "https://www.bol.com/nl/nl/l/alle-artikelen-aanbiedingen/58334/",
    "https://www.bol.com/nl/nl/ra/outlet/392102/",
    "https://www.bol.com/nl/nl/ra/outlet/392102/?page=2",
    "https://www.bol.com/nl/nl/p/-/9300000259370076/",
    "https://www.bol.com/nl/nl/l/muizen/7086/4294843152/",
    "https://www.bol.com/nl/s/?searchtext=laptop",
    "https://www.bol.com/be/nl/s/?searchtext=laptop",
    "https://m.bol.com/nl/nl/s/?searchtext=laptop"
  ],
  "fetchDetails": false,
  "maxReviews": 30,
  "maxListings": 20,
  "proxy": {
    "useApifyProxy": true
  },
  "residentialCountries": [],
  "preferDatacenter": false,
  "autoDowngradeProxy": true,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": [],
  "mcpConnectors": []
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `detail` (type: `string`):

No description

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

No description

## `changes` (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",
    "searchTerms": [
        "laptop"
    ],
    "productIds": [],
    "urls": [
        "https://www.bol.com/nl/nl/s/?searchtext=laptop",
        "https://www.bol.com/nl/nl/s/?searchtext=laptop&page=2",
        "https://www.bol.com/nl/nl/s/?searchtext=laptop&sort=price&sortOrder=asc&page=2",
        "https://www.bol.com/nl/nl/s/laptop/",
        "https://www.bol.com/nl/nl/s/.data?searchtext=laptop&page=1",
        "https://www.bol.com/nl/nl/p/acer-chromebook-cb314-4h-c4hf-intel-n100-14-full-hd-8-gb-lpddr5-128-gb-emmc/9300000263156289/",
        "https://www.bol.com/nl/nl/p/acer-chromebook-cb314-4h-c4hf-intel-n100-14-full-hd-8-gb-lpddr5-128-gb-emmc/9300000263156289/?bltgh=abc&promo=x",
        "https://www.bol.com/nl/nl/l/laptops/4770/",
        "https://www.bol.com/nl/nl/l/action-camera-s/4785/",
        "https://www.bol.com/nl/nl/b/acemagic/607284936/",
        "https://www.bol.com/nl/nl/l/alle-artikelen-aanbiedingen/58334/",
        "https://www.bol.com/nl/nl/ra/outlet/392102/",
        "https://www.bol.com/nl/nl/ra/outlet/392102/?page=2",
        "https://www.bol.com/nl/nl/p/-/9300000259370076/",
        "https://www.bol.com/nl/nl/l/muizen/7086/4294843152/",
        "https://www.bol.com/nl/s/?searchtext=laptop",
        "https://www.bol.com/be/nl/s/?searchtext=laptop",
        "https://m.bol.com/nl/nl/s/?searchtext=laptop"
    ],
    "proxy": {
        "useApifyProxy": true
    },
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "ignoreFieldsForChanges": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/bol-com-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",
    "searchTerms": ["laptop"],
    "productIds": [],
    "urls": [
        "https://www.bol.com/nl/nl/s/?searchtext=laptop",
        "https://www.bol.com/nl/nl/s/?searchtext=laptop&page=2",
        "https://www.bol.com/nl/nl/s/?searchtext=laptop&sort=price&sortOrder=asc&page=2",
        "https://www.bol.com/nl/nl/s/laptop/",
        "https://www.bol.com/nl/nl/s/.data?searchtext=laptop&page=1",
        "https://www.bol.com/nl/nl/p/acer-chromebook-cb314-4h-c4hf-intel-n100-14-full-hd-8-gb-lpddr5-128-gb-emmc/9300000263156289/",
        "https://www.bol.com/nl/nl/p/acer-chromebook-cb314-4h-c4hf-intel-n100-14-full-hd-8-gb-lpddr5-128-gb-emmc/9300000263156289/?bltgh=abc&promo=x",
        "https://www.bol.com/nl/nl/l/laptops/4770/",
        "https://www.bol.com/nl/nl/l/action-camera-s/4785/",
        "https://www.bol.com/nl/nl/b/acemagic/607284936/",
        "https://www.bol.com/nl/nl/l/alle-artikelen-aanbiedingen/58334/",
        "https://www.bol.com/nl/nl/ra/outlet/392102/",
        "https://www.bol.com/nl/nl/ra/outlet/392102/?page=2",
        "https://www.bol.com/nl/nl/p/-/9300000259370076/",
        "https://www.bol.com/nl/nl/l/muizen/7086/4294843152/",
        "https://www.bol.com/nl/s/?searchtext=laptop",
        "https://www.bol.com/be/nl/s/?searchtext=laptop",
        "https://m.bol.com/nl/nl/s/?searchtext=laptop",
    ],
    "proxy": { "useApifyProxy": True },
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "ignoreFieldsForChanges": [],
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/bol-com-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",
  "searchTerms": [
    "laptop"
  ],
  "productIds": [],
  "urls": [
    "https://www.bol.com/nl/nl/s/?searchtext=laptop",
    "https://www.bol.com/nl/nl/s/?searchtext=laptop&page=2",
    "https://www.bol.com/nl/nl/s/?searchtext=laptop&sort=price&sortOrder=asc&page=2",
    "https://www.bol.com/nl/nl/s/laptop/",
    "https://www.bol.com/nl/nl/s/.data?searchtext=laptop&page=1",
    "https://www.bol.com/nl/nl/p/acer-chromebook-cb314-4h-c4hf-intel-n100-14-full-hd-8-gb-lpddr5-128-gb-emmc/9300000263156289/",
    "https://www.bol.com/nl/nl/p/acer-chromebook-cb314-4h-c4hf-intel-n100-14-full-hd-8-gb-lpddr5-128-gb-emmc/9300000263156289/?bltgh=abc&promo=x",
    "https://www.bol.com/nl/nl/l/laptops/4770/",
    "https://www.bol.com/nl/nl/l/action-camera-s/4785/",
    "https://www.bol.com/nl/nl/b/acemagic/607284936/",
    "https://www.bol.com/nl/nl/l/alle-artikelen-aanbiedingen/58334/",
    "https://www.bol.com/nl/nl/ra/outlet/392102/",
    "https://www.bol.com/nl/nl/ra/outlet/392102/?page=2",
    "https://www.bol.com/nl/nl/p/-/9300000259370076/",
    "https://www.bol.com/nl/nl/l/muizen/7086/4294843152/",
    "https://www.bol.com/nl/s/?searchtext=laptop",
    "https://www.bol.com/be/nl/s/?searchtext=laptop",
    "https://m.bol.com/nl/nl/s/?searchtext=laptop"
  ],
  "proxy": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": []
}' |
apify call abotapi/bol-com-scraper --silent --output-dataset

```

## MCP server setup

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