DEV Community

Cover image for How to Scrape bol.com Product Prices and Sellers with Python
ReventaData
ReventaData

Posted on Originally published at apify.com

How to Scrape bol.com Product Prices and Sellers with Python

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:

  1. Python 3.11+ installed on your local development machine (apify-client 3.x requires it).
  2. The Apify API client for Python (pip install apify-client).
  3. 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
Enter fullscreen mode Exit fullscreen mode

Set your API token in your shell environment:

export APIFY_API_TOKEN="your_apify_token_here"
Enter fullscreen mode Exit fullscreen mode

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,
}
Enter fullscreen mode Exit fullscreen mode

(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:

  • maxItems acts as a hard budget ceiling: If you provide three start URLs and set maxItems: 50, the scraper terminates as soon as 50 items are collected across all lists.
  • Rating filters: Setting minRating: 4.5 drops 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 small maxItems yields 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")
Enter fullscreen mode Exit fullscreen mode

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 with maxItems: 60, maxPages: 4, sort: "RATING", and minRating: 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)
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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 as minimumQuantity, 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

  1. Volume discounts (bundlePrices): When a seller offers quantity prices, they land in bundlePrices (see the README example with minimumQuantity, price, and discountPercent).
  2. Language and region awareness: Starting URLs with /nl/nl/ yield Dutch text, while /be/fr/ URLs return French titles and delivery text.
  3. 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.
  4. Unrated listings: Unrated products have null for both rating and reviewCount. If your analysis requires customer feedback, keep minRating enabled.
  5. Start small: Run with maxItems: 5 first and read the rows before pointing the script at a whole category.
  6. Pick a seller side: Set seller to "BOL" or "PARTNER" to keep only listings sold by bol itself or only those from partner sellers; soldByBol in 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 minPrice or minRating thresholds 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)