Complete Guide: How to Scrape bol.com Product Data Using Python
Run the bol.com Scraper Apify Actor from Python, extract clean product and seller data, and export to JSON without browser maintenance.

This complete guide to bol.com Scraper shows how to run a bol.com scraper from Python through the Apify API and get back structured, typed product records: product titles, prices in euros, reference prices, discount percentages, seller identities, delivery dates, and customer star ratings. The behaviour described below is drawn directly from empirical runs and dataset inspections.
bol.com is a Dutch and Belgian online marketplace, so price trackers and catalog tools for the Netherlands and Belgium often need its data. A custom crawler meets two obstacles first: bol.com answers datacenter proxy addresses with HTTP 403, and its category navigation stops at 500 pages.
In this tutorial, you will learn how to automate bol.com data extraction using the bol.com Scraper Apify Actor (reventadata/bol-scraper). Prerequisites are minimal: Python 3.11+, the official apify-client package, and an Apify API token. The live benchmark run supporting this guide (configured with maxItems: 60, maxPages: 4, sort: "RATING", and minRating: 4.5) collected 60 structured product records in 40.2 seconds.
TL;DR
In this tutorial, you will learn to configure and trigger the bol.com Scraper Apify Actor from Python, inspect run execution states, extract clean dataset items, and handle volume discounts. Key steps: install apify-client, export your APIFY_API_TOKEN, define target category URLs in startUrls, call the Actor, and iterate over items in run.default_dataset_id.
Expected outcome: a JSON list of bol.com products with price, reference price, seller, delivery promise and rating for each, ready to load into a spreadsheet or database.
The Actor is available at apify.com/reventadata/bol-scraper.
Prerequisites and setup
Before running our extraction script, verify the following prerequisites:
To follow this tutorial, you will need:
- Python 3.11+ installed locally (
apify-client3.x requires it). - The Apify API client for Python (
pip install "apify-client~=3.0"). - An Apify account and API token.
An Apify account. Apify Console is the web interface where runs, datasets, and tokens are managed; your token lives on the API & Integrations page in Apify Console. The platform documentation index is at docs.apify.com.
Install the Apify client library via pip:
pip install "apify-client~=3.0"
Set your API token in your environment:
export APIFY_API_TOKEN="your_apify_token_here"
Understanding bol.com Scraper
How it works behind the scenes
The bol.com Scraper is architected specifically around the technical characteristics of the bol.com e-commerce platform:
- URL Taxonomy & Page Routing:
- The Actor reads category pages (
/nl/nl/l/<category-name>/<category-id>/) and brand pages (/nl/nl/b/<brand-name>/<brand-id>/). - The Actor normalizes start URLs to scheme, host, and path (category
/l/or brand/b/), ignoring any query strings. Filters such as price ranges, minimum rating, discount status, and seller type are configured through input parameters and applied after the fetch. - Bol.com's robots.txt disallows search query pages (
/s/). To maintain clean compliance and sustainable throughput, the Actor operates exclusively on open category and brand endpoints.
- The Actor reads category pages (
- Residential Proxy Transport:
- Data Extraction & Price Parsing:
- Instead of scraping rendered HTML elements, the Actor extracts structured JSON payloads embedded within the category pages.
- It parses tiered quantity discounts (
bundlePrices), manufacturer reference figures (Adviesprijs), the most shown non-promotion price of the last 90 days (Meestal), and merchant identities.
The execution pipeline flows linearly: your Python client invokes the Apify API, which spins up a dedicated container running reventadata/bol-scraper. The Actor queries bol.com endpoints through residential proxies in the Netherlands, extracts embedded structured data, pushes extracted records to the Apify dataset, and returns the dataset ID to your script.
Deep Dive: Understanding Reference Prices and Discounts on bol.com
On bol.com, price presentation includes distinct baseline classifications that the Actor captures into separate fields:
price: The selling price in EUR of the best offer shown for the product.referencePriceandreferencePriceLabel:"Meestal": The most shown non-promotion price of the last 90 days."Adviesprijs": Represents the manufacturer's recommended list price.
referencePriceCrossedOutanddiscountPercent:- When bol.com displays the reference price crossed out above the price, the scraper sets
referencePriceCrossedOut: trueand passes through bol's stated discount percentage indiscountPercent(null when bol states none).
- When bol.com displays the reference price crossed out above the price, the scraper sets
Handling Multi-Country Regional Variations
Bol.com serves the Netherlands and Belgium. The website taxonomy reflects this distinction in URL path segments:
- Netherlands (Dutch):
https://www.bol.com/nl/nl/l/... - Belgium (Dutch):
https://www.bol.com/be/nl/l/... - Belgium (French):
https://www.bol.com/be/fr/l/...
The scraper inspects the domain path of each start URL and preserves locale context:
- The
countryfield records"NL"or"BE". - The
languagefield records"nl"or"fr". - Titles and
deliverypromises follow the language of the source endpoint: Dutch listings state Dutch text (such asmorgen in huis), while French-language Belgian endpoints (/be/fr/) return French titles and delivery text. Promotionallabelscan stay in Dutch ("Budgettopper").
Step-by-step walkthrough
Step 1: Navigating to the Actor
Visit the bol.com Scraper Store page. The page details input schemas, pricing parameters, and author documentation. From here, you can click Try for free to inspect the configuration dashboard in Apify Console.
Step 2: Defining inputs and target categories
Navigate to bol.com in your browser, browse to your target category, and copy the full URL. For this guide, we use top-rated dishwashers: https://www.bol.com/nl/nl/l/vaatwassers/11067/
We configure our input dictionary with category filters, setting a minimum customer rating of 4.5 and sorting by rating:
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: While this introductory snippet specifies maxItems: 20 and maxPages: 1 for a quick test, our live benchmark run was configured with maxItems: 60, maxPages: 4, sort: "RATING", and minRating: 4.5.)
Step 3: Executing via Apify Python Client
Using client.actor("reventadata/bol-scraper").call(run_input=run_input) triggers execution in an isolated cloud container. The client polls until execution finishes and returns the populated run object:
# 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")
Execution characteristics:
- It iterates across category pagination up to
maxPagesor untilmaxItemsis satisfied. - In our test run (configured with
maxItems: 60,maxPages: 4,sort: "RATING", andminRating: 4.5), the Actor completed 60 products in 40.2 seconds.
Step 4: Iterating and processing dataset items
Once the run reaches SUCCEEDED, fetch the dataset items. The client provides methods to list all items or iterate over large datasets without loading everything into memory simultaneously:
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)
Verbatim dataset sample
Below is a complete, unedited record extracted during the 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"
}
A row with a discount looks like this. It comes from a saved bol.com page (2026-10-21) and shows the reference-price fields filled in:
{
"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"
}
Detailed schema breakdown
Each dataset record provides comprehensive e-commerce attributes:
| Field | Type | Description |
|---|---|---|
id |
string | bol.com product id string (16 digits). |
title |
string | Complete product listing title. |
url |
string | Canonical product URL. |
brand |
string / null | Extracted brand name. |
brandUrl |
string / null | URL to the brand's directory page on bol.com. |
price |
number | Selling price in EUR of the best offer shown for the product. |
currency |
string | Always "EUR". |
referencePrice |
number / null | Reference price in euros shown next to the price, when there is one (see referencePriceLabel and referencePriceCrossedOut). |
referencePriceLabel |
string / null | Classification label ("Meestal" or "Adviesprijs"). |
referencePriceCrossedOut |
boolean | True when bol displays the reference price crossed out. |
discountPercent |
integer / null | Discount percentage against the crossed-out reference price as stated by bol; null when bol states none. |
bundlePrices |
array | Quantity discounts: minimumQuantity, price per item in euros, and discountPercent. |
selectPrice |
number / null | Price for bol Select members, when it differs. |
seller |
string | Name of the seller of the best offer; bol itself is named bol. |
sellerId |
string | Merchant unique ID ("0" indicates bol.com direct). |
soldByBol |
boolean | True when bol itself sells the best offer. |
delivery |
string | Displayed delivery promise text. |
deliveryDate |
string | Latest estimated delivery date formatted as YYYY-MM-DD. |
rating |
number / null | Average user rating from 1.0 to 5.0. |
reviewCount |
integer / null | Number of customer ratings, null when unrated. |
categories |
array | Hierarchical category taxonomy path. |
scrapedAt |
string | ISO 8601 extraction timestamp. |
Advanced filtering and volume pricing
- Volume Tier Discounts (
bundlePrices): When a seller offers quantity prices, they land inbundlePrices(see the README example): each element providesminimumQuantity,priceper item in euros, anddiscountPercent. - Differentiating Seller Types: Setting
seller: "BOL"restricts output to products retailed directly by bol.com itself (sellerId: "0"). Conversely, settingseller: "PARTNER"collects only third-party marketplace merchants.
Troubleshooting
When operating the bol.com Scraper in production, you may encounter edge cases. Here is how to diagnose and resolve them:
- 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. - HTTP 403 blocks on datacenter proxy configurations: Datacenter IPs receive HTTP 403 block pages from bol.com. Always use the Actor's default Apify residential proxy configuration in the Netherlands.
- Pagination limits at page 500: A listing can have at most 500 pages; requesting page 501 returns an HTTP 404 response. If you are scraping large categories containing thousands of items, divide your crawl into narrower subcategories or brand pages to capture the full catalog.
- Handling invalid URLs: Supply category (
/l/) or brand (/b/) links; query strings in start URLs are ignored by the Actor. Supplying search queries (/s/) or individual product detail pages (/p/) results in an immediate input validation error.
Best practices
Follow these operational recommendations to maximize reliability and cost efficiency:
- Set
maxItemsas a budget ceiling: UsemaxItemsto establish a strict ceiling on total products collected across all start URLs before terminating. - Filter upstream to eliminate per-result fees: Use
minRating,onlyDiscounted,seller, and price boundaries in your input. Items dropped before dataset storage incur no per-result event fees. - Select strategic sort orders: Use
sort: "PRICE_ASC"when monitoring price floors, orsort: "RATING"when isolating top customer-reviewed products. - Partition large catalogs across subcategories: To stay safely within the 500-page pagination limit on large departments, split crawls across specific subcategory or brand URLs.
- Handle nullable attributes defensively: Unrated products return
nullforratingandreviewCount. Unbranded items returnnullforbrand. Ensure ETL code handles nullable fields gracefully.
Production Integration Patterns & Data Warehousing
When incorporating the bol.com Scraper into production ETL pipelines, consider these deployment patterns:
- Scheduled Runs & Cron Triggers: In Apify Console, navigate to Schedules to trigger recurring runs at off-peak hours (such as 03:00 CET). Daily scheduled runs enable time-series tracking of merchant pricing and delivery changes.
- Webhooks and Event Alerts: In Apify Console, you can configure an Apify webhook to trigger on run completion. The webhook payload carries the run object, which includes
defaultDatasetId. Your server or Lambda handler can then use the Apify API client to fetch the dataset items and load them into your database or data warehouse. - Downstream Data Analysis: Because dataset records export as clean JSON or CSV, you can read them with standard data science libraries (for example, reading items into a Pandas DataFrame in your own analysis script) to track price trends and brand distribution over time.
Pricing model and cost transparency
For current pricing and details, see the Store page.
Conclusion and resources
With the bol.com Scraper and a few lines of Python, you get typed bol.com records for price monitoring, catalog tracking and market research.
Try bol.com Scraper yourself and let me know what you build!
I build and maintain scrapers for European marketplaces under the reventadata name on the Apify Store.
- 🚀 Actor on Apify Store: bol.com Scraper
- 📚 API Client Documentation: Apify Python Client Documentation
- 💬 Support & Custom Inquiries: Use the Issues tab on the Actor's Apify Store page for feature requests or category extensions.
About the author
- Hi, I’m the engineer behind ReventaData. I build automated web scrapers, reverse-engineer undocumented APIs, and design data pipelines on Apify that turn complex, unstructured websites into clean, reliable feeds. Here on Hashnode, I share practical tutorials on modern web scraping with Python, bypassing aggressive anti-bot protections (Cloudflare, Akamai, PerimeterX), and data pipeline automation, reverse-engineering hidden endpoints, and automating data extraction. Explore our turnkey scrapers: https://apify.com/reventadata
Comments
Join the discussionThanks for the careful read, you are right on both points. The post now installs with pip install "apify-client~=3.0", because a plain pip install apify-client leaves an existing v2 in place and the code relies on the v3 typed run object. I also added a second sample row from a saved bol.com page that has a reference price, so the referencePrice, referencePriceLabel, referencePriceCrossedOut and discountPercent fields are shown filled in.
the reference price section is worth a closer look since your sample record leaves those fields empty. pinning to major v3 is the safer route, cause pip won’t upgrade an existing v2 install on its own. the schema table is missing 13 fields that appear in your own record, such as unitprice, condition and labels.
also the table of contents lists a pricing section and a conclusion that aren’t in the body. the dataset snippet loads everything in one call without checking the total, so iterate_items() with a count check would match the memory claim better




