By the end of this tutorial, you will have a working Python script that extracts bol.com category and brand listings into structured records: product title, selling price in EUR, reference price, discount percentage, seller name, delivery promise, rating, and review count.
bol.com is an online marketplace serving the Netherlands and Belgium. Collecting listings manually is impractical at scale, while building a custom scraper requires handling anti-bot protections and residential proxy rotation.
In this guide, we use Python and the Apify Actor bol.com Scraper (reventadata/bol-scraper) to extract catalog data with automated retries and residential proxies. Our benchmark run (oiRAQfzMlxspGYB2r, configured with maxItems: 60, maxPages: 4, sort: "RATING", and minRating: 4.5) took 40.2 seconds to extract 60 dishwasher listings into a clean dataset.
⏱️ Time estimate: About 10 minutes. The extraction run takes under a minute; installing the client and running the script takes the rest.
Prerequisites
To follow this tutorial, you will need:
-
Python 3.11+ installed on your local development machine (
apify-client3.x requires it). - The Apify API client for Python (
pip install apify-client). - An Apify account and API token.
Sign up for a free Apify account with email or GitHub (the free plan includes $5 of usage a month). Your personal API token lives on the API & Integrations page in Apify Console. We will load it safely from an environment variable rather than hardcoding credentials into our source code.
pip install apify-client
Set your API token in your shell environment:
export APIFY_API_TOKEN="your_apify_token_here"
What you'll achieve
By completing this guide, you will extract live bol.com category or brand listings into a dataset of typed records: price, reference price, seller, delivery promise, rating and image for each product. The section "Understanding your results" below lists the main fields.
Note on Data Boundaries: This Actor extracts data directly from category (
/l/) and brand (/b/) directory pages. It focuses on the primary offer displayed on the listing page. It does not navigate into individual product detail pages or collect competitor offers.
Example use case: Suppose you want to monitor pricing and seller distribution for appliances across the Netherlands. By giving the scraper a target category URL, you receive an automated export detailing which items are sold by third parties versus bol direct, their current discount depth, and stock delivery promises.
Step 1: Access bol.com Scraper
Navigate to the bol.com Scraper page on Apify Store.
You can explore the documentation, inspect input parameters, and review sample output records. If you are not logged in, clicking Try for free prompts you to register via email or GitHub. You are then directed to the Actor's console workspace where you can run manual extractions directly through a web form. In this tutorial, however, we use Python to automate the process programmatically.
Step 2: Configure category or brand URLs
The Actor accepts bol.com category pages and brand pages from both the Netherlands (/nl/nl/) and Belgium (/be/nl/ and /be/fr/).
Open bol.com in your web browser, navigate to your desired department (for example, Dishwashers: https://www.bol.com/nl/nl/l/vaatwassers/11067/), and copy the URL. Notice that bol.com URLs follow structured path patterns:
- Category pages contain
/l/<slug>/<id>/ - Brand pages contain
/b/<slug>/<id>/
Any query string in a start URL is ignored: the Actor normalizes each target to scheme, host, and path without query parameters. Filtering parameters (such as price limits, rating thresholds, and seller type) are configured as Actor inputs and applied to records after the fetch.
Note on Site Guidelines: The scraper targets category and brand pages. Keyword search result pages (
/s/) are deliberately excluded to respect website access rules.
In our Python script, we initialize the Apify client and pass our start URL in run_input:
import os
from apify_client import ApifyClient
# 1. Initialize ApifyClient with token from environment
client = ApifyClient(os.environ["APIFY_API_TOKEN"])
# 2. Define the extraction parameters
run_input = {
"startUrls": [
{"url": "https://www.bol.com/nl/nl/l/vaatwassers/11067/"}
],
"maxItems": 20,
"maxPages": 1,
"sort": "RATING",
"minRating": 4.5,
}
(Note: This demonstration snippet uses maxItems: 20 and maxPages: 1 for a fast initial test. The benchmark run oiRAQfzMlxspGYB2r cited in this guide ran with maxItems: 60, maxPages: 4, sort: "RATING", and minRating: 4.5.)
Step 3: Configure optional settings and filters
The Actor provides filtering options that execute upstream on the scraping container before records are saved to your dataset. Because the Actor charges per saved dataset item, filtering unneeded rows avoids per-result event fees:
| Parameter | Type | Default | Description |
|---|---|---|---|
startUrls |
array | Required | List of bol.com category or brand page URLs. |
maxItems |
integer | 100 |
Total products to collect across all start URLs before terminating. |
maxPages |
integer | 5 |
Maximum pagination depth per start URL (1 to 500). |
sort |
string | "POPULARITY" |
Sort order: "POPULARITY", "PRICE_ASC", "PRICE_DESC", or "RATING". |
minRating |
number | None |
Minimum customer review score (0 to 5.0). Drops unrated items when set. |
minPrice & maxPrice
|
number | None |
Price boundaries in EUR. |
onlyDiscounted |
boolean | false |
When true, retains only items with crossed-out reference prices. |
seller |
string | "ANY" |
Filters by seller type: "ANY", "BOL", or "PARTNER". |
includeSponsored |
boolean | false |
Whether to include promotional sponsored advertising tiles. |
Practical filtering recommendations:
-
maxItemsacts as a hard budget ceiling: If you provide three start URLs and setmaxItems: 50, the scraper terminates as soon as 50 items are collected across all lists. -
Rating filters: Setting
minRating: 4.5drops lower-rated listings before they are saved to the dataset, so you are not charged for them. -
Sorting for immediate insight: Using
sort: "PRICE_ASC"combined with a smallmaxItemsyields the cheapest offerings in a category without pagination overhead.
Step 4: Run the Actor
With the configuration ready, trigger the run using client.actor("reventadata/bol-scraper").call(run_input=run_input). This command starts the Actor in Apify's cloud environment, streams log events, and waits until execution completes:
# 3. Start the Actor run and wait for it to complete
print("Starting bol.com extraction...")
run = client.actor("reventadata/bol-scraper").call(run_input=run_input)
if run is None or run.status != "SUCCEEDED":
status = run.status if run else "unknown"
raise SystemExit(f"Run did not succeed (status: {status}).")
# 4. Fetch the resulting dataset items
dataset = client.dataset(run.default_dataset_id).list_items().items
print(f"Extraction complete! Retrieved {len(dataset)} products.\n")
Behind the scenes:
- The Actor launches in a dedicated container equipped with residential proxy rotation. Plain requests and datacenter IPs receive HTTP 403 block pages from bol.com; the Actor routes requests through residential proxies in the Netherlands.
- It parses the structured payload embedded within the category pages, reads prices in euros, extracts quantity prices when a seller offers them, and pushes clean records to the Apify dataset.
- In our live benchmark run (
oiRAQfzMlxspGYB2r), configured withmaxItems: 60,maxPages: 4,sort: "RATING", andminRating: 4.5, the Actor collected 60 products in 40.2 seconds.
Step 5: View and download results
Once the run completes, iterate over dataset to process each item in Python, or export the data via the Apify Console in JSON, CSV, or Excel format:
for item in dataset:
title = item.get("title")
price = item.get("price")
seller = item.get("seller")
rating = item.get("rating")
review_count = item.get("reviewCount")
delivery = item.get("delivery")
print(f"Product: {title}")
print(f" Price: €{price}")
print(f" Seller: {seller}")
if rating is None:
print(" Rating: not rated yet")
else:
print(f" Rating: {rating} / 5.0 ({review_count} reviews)")
print(f" Delivery: {delivery}")
print("-" * 50)
Here is a verbatim sample record extracted during our benchmark run:
{
"id": "9300000184000912",
"title": "Labirent LMV8-7PZW1 - Mini Vaatwasser - 8 Couverts - 7 Programma’s - Modern - Kleine Vrijstaande Afwasmachine - Zwart",
"url": "https://www.bol.com/nl/nl/p/labirent-ddd08-55bm-mini-vaatwasser-8-couverts-kleine-vrijstaande-vaatwasser-zwart/9300000184000912/",
"brand": "Labirent",
"brandUrl": "https://www.bol.com/nl/nl/b/labirent/604508015/",
"price": 299.95,
"currency": "EUR",
"referencePrice": null,
"referencePriceLabel": null,
"referencePriceCrossedOut": false,
"discountPercent": null,
"unitPrice": null,
"unitMeasure": null,
"bundlePrices": [],
"selectPrice": null,
"seller": "Labirent",
"sellerId": "1698826",
"soldByBol": false,
"condition": "new",
"delivery": "Op voorraad. Voor 17:00 uur besteld, morgen in huis",
"deliveryDate": "2026-10-24",
"isScarce": false,
"isSelect": false,
"labels": [],
"rating": 4.9,
"reviewCount": 39,
"categories": [
"Huishouden",
"Vaatwassen",
"Vaatwassers",
"Vrijstaande vaatwassers"
],
"packQuantity": null,
"image": "https://media.s-bol.com/m4vZ3Ovy79RR/76DpV28/550x589.jpg",
"isSponsored": false,
"country": "NL",
"language": "nl",
"sourceUrl": "https://www.bol.com/nl/nl/l/vaatwassers/11067/",
"page": 1,
"scrapedAt": "2026-10-23T08:17:28.906183+00:00"
}
Understanding your results
Each record follows the Actor's dataset schema:
-
id,title,url: bol.com product id (a string such as"9300000184000912"), title in the listing's language, product page link. -
brand,brandUrl: brand name and brand page, null when unbranded. -
price,currency: selling price of the primary offer, always"EUR". -
referencePrice,referencePriceLabel: the price bol shows next to it, labelled"Meestal"(the most shown non-promotion price of the last 90 days) or"Adviesprijs"(manufacturer's recommended price). -
referencePriceCrossedOut,discountPercent: whether bol crosses the reference out, and bol's own stated discount (null when bol states none). -
bundlePrices: quantity prices asminimumQuantity,price,discountPercent. -
seller,sellerId,soldByBol: the offering merchant; seller id"0"is bol itself. -
condition:"new"on all 60 benchmark rows. -
delivery,deliveryDate: delivery promise text and latest arrival date (YYYY-MM-DD). -
isScarce,isSelect,labels: scarcity flag, bol Select badge, promotional badges such as"Budgettopper". -
rating,reviewCount: average rating and number of ratings, null when unrated. -
categories,image,scrapedAt: breadcrumb path, primary product image, extraction timestamp.
Pro tips & edge cases
-
Volume discounts (
bundlePrices): When a seller offers quantity prices, they land inbundlePrices(see the README example withminimumQuantity,price, anddiscountPercent). -
Language and region awareness: Starting URLs with
/nl/nl/yield Dutch text, while/be/fr/URLs return French titles and delivery text. - 500-page limit: A listing can have at most 500 pages (page 501 returns HTTP 404). If you are scraping large categories, divide your crawl into narrower subcategories or brand pages to capture the full catalog.
-
Unrated listings: Unrated products have
nullfor bothratingandreviewCount. If your analysis requires customer feedback, keepminRatingenabled. -
Start small: Run with
maxItems: 5first and read the rows before pointing the script at a whole category. -
Pick a seller side: Set
sellerto"BOL"or"PARTNER"to keep only listings sold by bol itself or only those from partner sellers;soldByBolin each row tells them apart.
Troubleshooting
-
Empty dataset / Run marked FAILED: To protect downstream data pipelines from silent failures, the Actor deliberately fails if zero rows match your parameters. If this occurs, verify that your start URL is valid and that your
minPriceorminRatingthresholds are not too restrictive. - Proxy blocks on custom configurations: Bol.com blocks datacenter IP ranges with HTTP 403. Always use the default Apify residential proxy configuration in the Netherlands.
-
Handling invalid URLs: Ensure you supply category (
/l/) or brand (/b/) links. Supplying search queries or product detail pages will result in an immediate input validation error.
Pricing and Next Steps
The bol.com Scraper is pay-per-event: $1.00 per 1,000 results plus $0.00005 per run start at the default 512 MB of memory; see the Store page for current pricing. Rows your filters drop are not charged.
You can try the Actor right now in your browser or run it programmatically in your data pipelines:
What products or categories are you planning to scrape from bol.com? Let us know in the comments below!
Top comments (0)