# bol.com API scraper - search, product detail, marketplace seller offers and category browse for bol.com, the leading online retailer in the Netherlands and Belgium. Prices in EUR, Dutch and French storefronts, no account, no browser.

> Search bol.com by keyword, paginated. Every row carries the product id and url, title, brand, the current price and bol's own 90-day reference price, the discount, the seller behind the buy-box (bol itself or a marketplace partner), the delivery promise, the rating and review count, the category path, the EAN when bol publishes one, and whether the row is a paid placement. The response also returns the ids of the categories the results fall into, which is what `category` takes.
> ReefAPI engine `bol` · 4 endpoints · clean JSON, no scraping or browsers to manage.

## How to call
- **Endpoint:** `POST https://api.reefapi.com/bol/v1/<action>` with a JSON body.
- **Auth:** header `x-api-key: <YOUR_REEFAPI_KEY>` — create one free (1,000 credits, no card): https://reefapi.com/signup
- **Response (every call):** `{ ok: boolean, data: ..., meta: { record_count, credits, ... }, error: { code, message } }` — branch on `ok`. Failed or blocked calls are free.
- **One key + one shared credit pool** across every ReefAPI API. Per-call credits are listed on each endpoint below.
- **Use it from an AI agent (MCP):** connect `https://api.reefapi.com/mcp` (remote streamable-http, `Authorization: Bearer <key>`) and your assistant can call these actions directly.

## Endpoints

### POST /bol/v1/search — 2 credits
Search bol.com by keyword, paginated. Every row carries the product id and url, title, brand, the current price and bol's own 90-day reference price, the discount, the seller behind the buy-box (bol itself or a marketplace partner), the delivery promise, the rating and review count, the category path, the EAN when bol publishes one, and whether the row is a paid placement. The response also returns the ids of the categories the results fall into, which is what `category` takes.

**Parameters:**
- `query` (string, required) — Search keyword, in the storefront's language (Dutch for nl and be, French for be-fr).
- `page` (integer, optional, default 1) — Result page, 1-based, ~30 products per page. bol serves at most 10 search pages for a query - page 11 comes back empty - so narrow the query rather than paging past it. Category browse pages go much deeper (see `total_pages`).
- `sort` (enum, optional) — Result ordering. Only these three are published because only these three were measured to actually re-order bol's results; every other ordering the site offers is applied client-side and is not reachable from a request. [one of: relevance, price_asc, price_desc]
- `market` (enum, optional, default "nl") — Which bol storefront to read: nl (bol.com/nl/nl, Dutch) - be (bol.com/be/nl, Belgian Dutch) - be-fr (bol.com/be/fr, Belgian French). One catalogue, three localised storefronts; prices are in EUR on all three and can differ between them. [one of: nl, be, be-fr]
- `max_rotations` (integer, optional, default 4) — Advanced: how many times to retry a refused request (1-10, default 4). Leave unchanged for typical use.

**Returns:** products[]{product_id, title, url, brand{}, price, currency, reference_price, discount_percent, condition, seller{id, name, is_bol, rating, rating_count (null — bol publishes a seller score, not a count), top_seller}, delivery{description, arrives_by, shipping_cost, within_48h}, low_stock, promotions[], rating, review_count, image, category, category_path[], ean, highlights[], sponsored, offers_url}, count, page, total_results, total_results_is_estimate, total_pages, categories[]{id, name, url}

**Example request body:**
```json
{
  "query": "koptelefoon"
}
```

### POST /bol/v1/product/detail — 2 credits
Full bol.com product by id (or by its url): title, brand, EAN and MPN, the buy-box price with bol's 90-day reference price and the discount, the seller, the delivery promise, the full image gallery, the complete specification table, the description as plain text, the category breadcrumb, the average rating and review count, and how many competing offers exist. Review TEXT is not returned - bol serves it only to a signed-in session, and this endpoint never invents it.

**Parameters:**
- `product_id` (string, required) — bol product id - the long number at the end of a product url (bol.com/nl/nl/p/<slug>/9300000180008278/). A full product url works too: the words in the path are decoration and the number is the key. Every row `search` and `category` return carries both.
- `market` (enum, optional, default "nl") — Which bol storefront to read: nl (bol.com/nl/nl, Dutch) - be (bol.com/be/nl, Belgian Dutch) - be-fr (bol.com/be/fr, Belgian French). One catalogue, three localised storefronts; prices are in EUR on all three and can differ between them. [one of: nl, be, be-fr]
- `max_rotations` (integer, optional, default 4) — Advanced: how many times to retry a refused request (1-10, default 4). Leave unchanged for typical use.

**Returns:** product{product_id, title, subtitle, url, brand{id, name, url}, ean, mpn, model, colour, price, currency, reference_price, discount_percent, discount_amount, condition, seller{}, delivery{}, low_stock, promotions[], rating, review_count, rating_scale (null — bol publishes no scale for the product rating; its SELLER score is a separate 1-10 scale on seller.rating), description, highlights[], specifications[]{group, key, name, values[]}, images[], video, category, category_path[], breadcrumb[]{id, name, url}, offers_url, offer_count, related_products[]}

**Example request body:**
```json
{
  "product_id": "9300000180008278"
}
```

### POST /bol/v1/product/offers — 1 credit
Every seller offering one bol.com product, with that seller's price, condition, delivery promise, shipping cost and bol seller rating - bol is a marketplace and this is the competing-offer table behind the buy-box, on bol's own price-overview page. Cheap: about 45 KB per call against 290 KB for the product page.

**Parameters:**
- `product_id` (string, required) — bol product id - the long number at the end of a product url (bol.com/nl/nl/p/<slug>/9300000180008278/). A full product url works too: the words in the path are decoration and the number is the key. Every row `search` and `category` return carries both.
- `market` (enum, optional, default "nl") — Which bol storefront to read: nl (bol.com/nl/nl, Dutch) - be (bol.com/be/nl, Belgian Dutch) - be-fr (bol.com/be/fr, Belgian French). One catalogue, three localised storefronts; prices are in EUR on all three and can differ between them. [one of: nl, be, be-fr]
- `max_rotations` (integer, optional, default 4) — Advanced: how many times to retry a refused request (1-10, default 4). Leave unchanged for typical use.

**Returns:** product{product_id, title, url, image}, offers[]{offer_id, price, currency, reference_price, discount_percent, condition, offer_type, seller{id, name, is_bol, rating, rating_count (null — bol publishes a seller score, not a count), top_seller}, delivery{description, arrives_by, shipping_cost, within_48h}, low_stock, promotions[]}, count, offer_counts{new, refurbished, secondhand}

**Example request body:**
```json
{
  "product_id": "9300000180008278"
}
```

### POST /bol/v1/category — 2 credits
Browse a bol.com category by its id, paginated - the same rich product row `search` returns, plus the category bol resolved and its breadcrumb. Category listings page far deeper than search does and their result total is exact rather than estimated.

**Parameters:**
- `category_id` (string, required) — bol category id - the number in a category url (bol.com/nl/nl/l/laptops/4770/). A full category url works too. The NUMBER decides the category and the words are ignored, so /l/koptelefoons/4770/ really does serve Laptops; the response echoes the category bol resolved. `search` returns the ids of the categories its results fall into, under `categories`.
- `page` (integer, optional, default 1) — Result page, 1-based, ~30 products per page. Category browse pages go deep - the response reports `total_pages`.
- `sort` (enum, optional) — Result ordering. Only these three are published because only these three were measured to actually re-order bol's results; every other ordering the site offers is applied client-side and is not reachable from a request. [one of: relevance, price_asc, price_desc]
- `market` (enum, optional, default "nl") — Which bol storefront to read: nl (bol.com/nl/nl, Dutch) - be (bol.com/be/nl, Belgian Dutch) - be-fr (bol.com/be/fr, Belgian French). One catalogue, three localised storefronts; prices are in EUR on all three and can differ between them. [one of: nl, be, be-fr]
- `max_rotations` (integer, optional, default 4) — Advanced: how many times to retry a refused request (1-10, default 4). Leave unchanged for typical use.

**Returns:** category{id, name, url, path[]}, products[]{...same row as search}, count, page, total_results, total_results_is_estimate, total_pages

**Example request body:**
```json
{
  "category_id": "4770"
}
```

## More
- Try it live, no code: https://reefapi.com/playground?engine=bol
- Human docs page: https://reefapi.com/docs/bol
- Overview page: https://reefapi.com/bol-api
- Every ReefAPI API in one file (for your AI): https://reefapi.com/llms-full.txt
