# bol.com Scraper: Prices, Sellers & Ratings (`reventadata/bol-scraper`) Actor

Scrape bol.com category and brand pages in the Netherlands and Belgium. One row per product: price, reference price and discount, quantity prices, seller, delivery, rating, review count and category. Sort by price or rating, filter by seller, price and keywords.

- **URL**: https://apify.com/reventadata/bol-scraper.md
- **Developed by:** [reventadata](https://apify.com/reventadata) (community)
- **Categories:** E-commerce, Automation
- **Stats:** 2 total users, 1 monthly users, 50.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 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.

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: Prices, Sellers and Ratings by Category or Brand

> **Unofficial.** This Actor is not affiliated with, authorised by, endorsed by, or in any way
> officially connected to bol.com. "bol" and "bol.com" are trademarks of their respective owner and are
> used here only to describe what this tool reads. It reads the same public category and brand pages a browser does.

Give it bol.com category or brand pages (Netherlands or Belgium) and get one clean row per product: price in
euros, the reference price and discount, quantity-discount prices, seller, delivery promise, rating, review count
and category path.

### What does it do?

For each start URL it walks the result pages of that bol.com category or brand page, reads the product data the
page embeds, and saves one row per product. A page holds 24 to 30 products and the site ends every list after
page 500.

It reads **category pages** (`https://www.bol.com/nl/nl/l/<name>/<id>/`, including filtered category pages copied
from the site) and **brand pages** (`https://www.bol.com/nl/nl/b/<name>/<id>/`). It does not read search result
pages or product pages: bol.com's robots.txt disallows search (`/s/`), and this Actor stays on the two page types
it leaves open.

### Why use it?

- **Prices as shoppers see them.** `price` is the selling price of the offer bol shows for the product, in euros.
- **Reference price, labelled.** `referencePrice` comes with `referencePriceLabel` (`Meestal`, the most shown non-promotion
  price of the last 90 days as bol defines it, or `Adviesprijs`, the manufacturer's recommended price) and `referencePriceCrossedOut`, so a
  real discount is told apart from a comparison price.
- **Quantity discounts and Select prices.** `bundlePrices` lists the price per item at 2, 3, 4 or more pieces;
  `selectPrice` is the bol Select member price when bol shows one.
- **Who sells it.** `seller`, `sellerId` and `soldByBol` tell bol's own offers from marketplace partners.
- **Filters that run before you pay.** Price band, minimum rating, discounted only, seller type, words the title,
  brand or category must or must not contain.
- **Cheapest or dearest first.** Sort by price (either way) and a small `maxItems` returns the top of the list;
  the rating sort is bol's own best-rated order.

### How to use it

1. Put one or more bol.com category or brand URLs in `startUrls`.
2. Set `maxItems` (rows to save) and, if you want, a sort order and filters.
3. Run the Actor and download the dataset as JSON, CSV or Excel.

#### Input

| Field | Type | Description |
|---|---|---|
| `startUrls` | array | Category (`/l/`) or brand (`/b/`) pages on bol.com, Netherlands (`/nl/nl/`) or Belgium (`/be/nl/`, `/be/fr/`). Query strings are ignored: the Actor sets page and sort itself. |
| `maxItems` | integer | Total products to save across all start URLs, counted after the filters. Default 100. URLs run in the order given and the run stops once the cap is reached. |
| `maxPages` | integer | Result pages per start URL, 1 to 500. Default 5. |
| `sort` | string | `POPULARITY` (the site's default), `PRICE_ASC`, `PRICE_DESC` or `RATING`. |
| `minPrice`, `maxPrice` | number | Price band in euros. |
| `minRating` | number | Minimum average rating, 0 to 5. Unrated products are dropped when set. |
| `onlyDiscounted` | boolean | Keep only products whose reference price is shown crossed out above the price. |
| `seller` | string | `ANY`, `BOL` (sold by bol itself) or `PARTNER` (marketplace sellers). |
| `includeSponsored` | boolean | Sponsored tiles are advertisements the site pins to the first result pages. Dropped by default. |
| `includeKeywords`, `excludeKeywords` | array | Words the title, brand or category must / must not contain, ignoring case and accents. |
| `proxyConfiguration` | object | Apify residential proxy in the Netherlands (default, and the transport measured to work). No proxy and datacenter proxies get an "IP address is blocked" page. |

#### Sample input

```json
{
    "startUrls": [{"url": "https://www.bol.com/nl/nl/l/muurstickers/14158/"}],
    "maxItems": 20,
    "maxPages": 1,
    "sort": "PRICE_ASC"
}
```

#### Output

One row per product:

| Field | Type | Description |
|---|---|---|
| `id` | string | bol.com product id. |
| `title` | string | Product title. |
| `url` | string | Product page URL. |
| `brand`, `brandUrl` | string / null | Brand name and its bol.com page. |
| `price` | number | Selling price in EUR of the offer bol shows. |
| `currency` | string | Always `EUR`. |
| `referencePrice` | number / null | Reference price in EUR, when bol shows one. |
| `referencePriceLabel` | string / null | `Meestal` or `Adviesprijs`, in the page language. |
| `referencePriceCrossedOut` | boolean | True when bol shows the reference price crossed out. |
| `discountPercent` | integer / null | Discount against the crossed-out reference price, as bol states it. |
| `unitPrice`, `unitMeasure` | number / string | Price per unit and bol's code for the unit, or null. |
| `bundlePrices` | array | `minimumQuantity`, `price` per item and `discountPercent` for each quantity discount. |
| `selectPrice` | number / null | bol Select member price, when bol shows one. |
| `seller`, `sellerId`, `soldByBol` | string / string / boolean | Seller of the offer; seller id `0` is bol itself. |
| `condition` | string | `new`. |
| `delivery`, `deliveryDate` | string | The delivery promise as shown, and the latest delivery date (YYYY-MM-DD). |
| `isScarce`, `isSelect` | boolean | bol's low-stock flag and the Select label. |
| `labels` | array | Promotion and award labels on the tile, for example `deal` or `Budgettopper`. |
| `rating`, `reviewCount` | number / integer | Average rating out of 5 and number of ratings; both null when the product has none. |
| `categories` | array | Category path from the root to the deepest category. |
| `packQuantity` | integer / null | Pieces in the pack when bol states it. |
| `image` | string | Main image URL. |
| `isSponsored` | boolean | True when the tile was a sponsored placement. |
| `country`, `language` | string | `NL` or `BE`, and `nl` or `fr`, from the start URL. |
| `sourceUrl`, `page` | string / integer | The start URL the row came from and the result page. |
| `scrapedAt` | string | ISO 8601 timestamp. |

#### Sample output

Taken from a real row (2026-10-21), with `scrapedAt` set to a fixed time:

```json
{
    "id": "9200000090597626",
    "title": "12x Glow in the Dark kleurige 3D Vlinders met magneet en sticker, voor muur, koelkast, deuren etc.",
    "url": "https://www.bol.com/nl/nl/p/12x-glow-in-the-dark-kleurige-3d-vlinders-met-magneet-en-sticker-voor-muur-koelkast-deuren-etc/9200000090597626/",
    "brand": "Favorite Things",
    "brandUrl": "https://www.bol.com/nl/nl/b/favorite-things/15956481/",
    "price": 4.39,
    "currency": "EUR",
    "referencePrice": 7.45,
    "referencePriceLabel": "Meestal",
    "referencePriceCrossedOut": true,
    "discountPercent": 41,
    "unitPrice": null,
    "unitMeasure": null,
    "bundlePrices": [
        {"minimumQuantity": 2, "price": 4.26, "discountPercent": 3},
        {"minimumQuantity": 4, "price": 4.13, "discountPercent": 6},
        {"minimumQuantity": 6, "price": 3.95, "discountPercent": 10}
    ],
    "selectPrice": null,
    "seller": "Knaak",
    "sellerId": "1476045",
    "soldByBol": false,
    "condition": "new",
    "delivery": "Uiterlijk 28 oktober in huis",
    "deliveryDate": "2026-10-28",
    "isScarce": false,
    "isSelect": false,
    "labels": ["Budgettopper"],
    "rating": 4.2,
    "reviewCount": 30,
    "categories": ["Wonen", "Woonaccessoires", "Wanddecoratie", "Muurstickers"],
    "packQuantity": null,
    "image": "https://media.s-bol.com/nMgn4BNWK4E/550x550.jpg",
    "isSponsored": false,
    "country": "NL",
    "language": "nl",
    "sourceUrl": "https://www.bol.com/nl/nl/l/muurstickers/14158/",
    "page": 1,
    "scrapedAt": "2026-10-21T12:00:00+00:00"
}
```

### Limits

- **Category and brand pages only.** There is no keyword search and no product-page detail (no EAN, no full
  specifications, no list of competing offers). A row is what the result tile shows.
- **500 pages per start URL.** The site answers page 501 with a 404. On the `muurstickers` category (2026-10-21) pages 1
  to 5 held 29 to 30 products each, 6 of them sponsored, and pages 7 to 25 held 23 to 24 with none.
- **The site's result total is not a real count.** The `muurstickers` and `woonartikelen` category pages report a
  total of 300,000 and flag it as an early stop. Split a broad category with its sub-categories or brand pages.
- **One price per product.** `price` is the best offer bol shows on the tile; other sellers' offers for the same
  product are not listed.
- Prices and delivery promises are what the site serves to an anonymous visitor from the Netherlands.
- Text fields (title, delivery, labels) are in the language of the page: Dutch for `/nl/` and `/be/nl/`, French for `/be/fr/`.
- A run that saves zero rows fails instead of finishing empty, so a mistyped URL or an over-strict filter is visible.

### How fast is it?

About 5 seconds per result page. Measured 2026-10-21: 1,000 rows of the `woonartikelen` category (43 pages) in 220 seconds.

### How much does it cost to scrape bol.com?

Pay-per-event: **$0.001 per result** saved to the dataset (**$1.00 per 1,000 results**), plus **$0.00005** charged once
when the run starts. No subscription. Filters run before a row is saved, so rows they drop are not charged.

### FAQ

**Is a proxy required?** Yes. bol.com returns a 403 "IP address is blocked" page to plain and datacenter requests.
The Actor defaults to Apify residential proxy in the Netherlands. A residential exit is occasionally one bol.com has
already blocked; the Actor retries with a new exit.

**Why can I not search for a keyword?** bol.com's robots.txt disallows its search pages, so the Actor does not read
them. Use the category or brand page that holds the products you want.

**Why is `discountPercent` null when `referencePrice` is set?** bol shows two kinds of reference price. `Meestal` is
usually crossed out and comes with a percentage, but some `Meestal` prices are shown without it. `Adviesprijs` is the
manufacturer's recommended price, shown for comparison, not crossed out, with no percentage. Check `referencePriceCrossedOut`.

**Why are `rating` and `reviewCount` null?** The product has no ratings yet. In a 1,000-row run on 2026-10-21, 66 had none.

**Why does a row from a Belgian page have French text?** Pages under `/be/fr/` are in French. Prices are in euros either way.

**Does it use a bol account?** No. It sends no login and sees what an anonymous visitor sees.

### More scrapers

| Actor | What it scrapes |
|---|---|
| [Amazon Product & Price Scraper](https://apify.com/reventadata/amazon-scraper) | Amazon product listings and prices |
| [Idealo Price Comparison Scraper](https://apify.com/reventadata/idealo-scraper) | Idealo price comparison listings |
| [MediaMarkt Product & Price Scraper](https://apify.com/reventadata/mediamarkt-scraper) | MediaMarkt products and prices |
| [AliExpress Product & Price Scraper](https://apify.com/reventadata/aliexpress-scraper) | AliExpress products and prices |
| [Shopify Store Monitor](https://apify.com/reventadata/shopify-scraper) | Price and stock changes on Shopify stores |

# Actor input Schema

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

bol.com category pages (for example https://www.bol.com/nl/nl/l/muurstickers/14158/) or brand pages (https://www.bol.com/nl/nl/b/merkloos-sans-marque/8047476/), Netherlands (/nl/nl/) or Belgium (/be/nl/, /be/fr/). Filtered category pages copied from the site also work. Search result pages and product pages are not read.

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

Total number of products to save across all start URLs. A result page holds 24 to 30 products.

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

Result pages to walk per start URL. The site ends every list after page 500, about 12,000 products.

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

Order of the products on the site. Price orders let a small maxItems return the cheapest or dearest products first; the rating order is bol's own.

## `minPrice` (type: `number`):

Keep only products priced at or above this amount in euros. Leave empty for no minimum.

## `maxPrice` (type: `number`):

Keep only products priced at or below this amount in euros. Leave empty for no maximum.

## `minRating` (type: `number`):

Keep only products with an average customer rating at or above this value (0 to 5). Unrated products are dropped when this is set.

## `onlyDiscounted` (type: `boolean`):

Keep only products whose reference price is shown crossed out above the selling price. A recommended price (Adviesprijs) is not crossed out and does not count.

## `seller` (type: `string`):

Keep products sold by bol itself, products sold by marketplace partners, or both.

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

Sponsored tiles are advertisements the site pins to the first result pages. They are dropped unless this is on; each row says whether it was sponsored.

## `includeKeywords` (type: `array`):

Keep only products whose title, brand or category contains every one of these words, ignoring case and accents.

## `excludeKeywords` (type: `array`):

Drop products whose title, brand or category contains any of these words, ignoring case and accents.

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

Apify residential proxy in the Netherlands is required: no proxy and datacenter proxies get an 'IP address is blocked' page from bol.com.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.bol.com/nl/nl/l/muurstickers/14158/"
    }
  ],
  "maxItems": 20,
  "maxPages": 1,
  "sort": "POPULARITY",
  "onlyDiscounted": false,
  "seller": "ANY",
  "includeSponsored": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "NL"
  }
}
```

# Actor output Schema

## `dataset` (type: `string`):

Products read from bol.com listing pages.

# 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 = {
    "startUrls": [
        {
            "url": "https://www.bol.com/nl/nl/l/muurstickers/14158/"
        }
    ],
    "maxItems": 20,
    "maxPages": 1,
    "sort": "POPULARITY",
    "seller": "ANY",
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "NL"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("reventadata/bol-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 = {
    "startUrls": [{ "url": "https://www.bol.com/nl/nl/l/muurstickers/14158/" }],
    "maxItems": 20,
    "maxPages": 1,
    "sort": "POPULARITY",
    "seller": "ANY",
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "NL",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("reventadata/bol-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 '{
  "startUrls": [
    {
      "url": "https://www.bol.com/nl/nl/l/muurstickers/14158/"
    }
  ],
  "maxItems": 20,
  "maxPages": 1,
  "sort": "POPULARITY",
  "seller": "ANY",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "NL"
  }
}' |
apify call reventadata/bol-scraper --silent --output-dataset

```

## MCP server setup

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