Skip to main content

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.

Updated 13 min readMarkdown
Complete Guide: How to Scrape bol.com Product Data Using Python

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:

  1. Python 3.11+ installed locally (apify-client 3.x requires it).
  2. The Apify API client for Python (pip install "apify-client~=3.0").
  3. 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:

  1. 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.
  2. Residential Proxy Transport:
    • Datacenter IPs and standard non-residential requests receive HTTP 403 block pages from bol.com.
    • The Actor routes traffic through Apify's residential proxy network located in the Netherlands, and retries a page on a new exit when bol.com returns an unrecognised page.
  3. 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:

  1. price: The selling price in EUR of the best offer shown for the product.
  2. referencePrice and referencePriceLabel:
    • "Meestal": The most shown non-promotion price of the last 90 days.
    • "Adviesprijs": Represents the manufacturer's recommended list price.
  3. referencePriceCrossedOut and discountPercent:
    • When bol.com displays the reference price crossed out above the price, the scraper sets referencePriceCrossedOut: true and passes through bol's stated discount percentage in discountPercent (null when bol states none).

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 country field records "NL" or "BE".
  • The language field records "nl" or "fr".
  • Titles and delivery promises follow the language of the source endpoint: Dutch listings state Dutch text (such as morgen in huis), while French-language Belgian endpoints (/be/fr/) return French titles and delivery text. Promotional labels can 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 maxPages or until maxItems is satisfied.
  • In our test run (configured with maxItems: 60, maxPages: 4, sort: "RATING", and minRating: 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

  1. Volume Tier Discounts (bundlePrices): When a seller offers quantity prices, they land in bundlePrices (see the README example): each element provides minimumQuantity, price per item in euros, and discountPercent.
  2. Differentiating Seller Types: Setting seller: "BOL" restricts output to products retailed directly by bol.com itself (sellerId: "0"). Conversely, setting seller: "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 minPrice or minRating thresholds 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:

  1. Set maxItems as a budget ceiling: Use maxItems to establish a strict ceiling on total products collected across all start URLs before terminating.
  2. 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.
  3. Select strategic sort orders: Use sort: "PRICE_ASC" when monitoring price floors, or sort: "RATING" when isolating top customer-reviewed products.
  4. 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.
  5. Handle nullable attributes defensively: Unrated products return null for rating and reviewCount. Unbranded items return null for brand. 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:

  1. 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.
  2. 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.
  3. 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.

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

Thanks 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

More from this blog