# Bol Offers (`s-r/bol-offers`) Actor

Give it EAN/GTIN codes and get the bol.com buy box for each: winning seller, price, shipping, total price, condition and availability, in the Netherlands and Belgium. One row per EAN. bol.com publishes one winning offer per product, not a full seller list.

- **URL**: https://apify.com/s-r/bol-offers.md
- **Developed by:** [SR](https://apify.com/s-r) (community)
- **Categories:** E-commerce, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $15.00 / 1,000 bol-offers

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Bol.com Buy Box Scraper - Winning Offer by EAN

Give this Actor a list of EAN/GTIN codes and it returns the bol.com buy box for
each one: the winning seller, price, shipping, total price, condition and
availability, in the Netherlands and Belgium.

It reads a cache that ShoppingScraper keeps warm rather than hitting bol.com
live on every call, which has one consequence worth knowing up front.

### The first lookup is free

Every EAN comes back in one of three states:

| status | what it means | charged |
|---|---|---|
| `cached` | the buy box is ready | yes |
| `scraped_no_offers` | the scrape ran and bol.com had no offer for it | yes |
| `queued_for_scrape` | nothing known yet, a scrape has just been queued | **no** |

An EAN nobody has asked for before is scraped on demand, which takes about
five minutes. The run waits for that (up to `wait_for_scrape_seconds`, ten
minutes by default) and returns the offers in the same run, so a cold cache
normally costs you a few minutes, not a second run. A row only stays
`queued_for_scrape` when the scrape had not landed by the time the wait ran
out.

A `queued_for_scrape` row costs you nothing, and that is enforced by where it
is written rather than by a promise. Charged rows go to the **default dataset**,
which is what Apify bills per item. Queued rows never touch it: they are listed
in a `QUEUED_EANS` record in the key-value store, together with the list to
retry, and the run summary says how many are still open. Run the Actor again
for those EANs a few minutes later.

`scraped_no_offers` is charged on purpose: "nobody sells this on bol.com" is a
real answer, and it took a real scrape to establish.

### It is the buy box, not the seller list

bol.com publishes one winning offer per product plus a price range, so
`offers_count` is at most 1. If you need every seller on a listing, this is not
that Actor, and no setting here will change it.

### Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `eans` | array | **required** | EAN/GTIN codes. |
| `country` | string | `nl` | `nl` or `be`. Everything else is rejected. |
| `max_age_hours` | integer | `24` | How old a cached result may be and still count as a hit. |
| `wait_for_scrape_seconds` | integer | `600` | How long the run polls for EANs that had to be scraped first. `0` returns immediately. |
| `force` | boolean | `false` | Demand data newer than the TTL and trigger an immediate re-scrape. |
| `ttl_hours` | integer | `6` | Only used with `force`. |
| `concurrency` | integer | `10` | EANs looked up at once. |

**EAN only.** The endpoint does not accept `bpid`, `sku` or `product_id`; those
return an error. A bol product id passed in the EAN field is accepted and
queued, and the row is flagged `bpid_passthrough: true` so you can tell those
rows apart, but whether the upstream scraper resolves a bol id rather than a
GTIN is not something this Actor can promise.

### Output

One row per EAN, so the output joins straight back onto your input list through
`requested_identifier`, including rows that are still queued.

```json
{
  "channel": "bol",
  "ean": "8710398523082",
  "gl": "nl",
  "requested_identifier": "8710398523082",
  "found": true,
  "status": "scraped_no_offers",
  "billed": true,
  "offers_count": 0,
  "offers": []
}
```

### What this does not do

No product description, no rating, no review count. The offers endpoint carries
seller and price data, and this Actor passes through what is there.

`already_queued` on a queued row is global: true when anyone at all has a scrape
running for that combination, not only you. It is information, never a billing
signal.

### FAQ

**Why did my run return nothing?**
Either the EAN had not been scraped on bol.com yet and the scrape did not land
inside the wait (the row says `queued_for_scrape`, it was free, run again in a
few minutes), or the lookup itself failed. A failed lookup is never dressed up
as "no offers": it is listed in the `errors` record, and when every lookup
fails the run is marked failed with the reason.

**Why only one offer?**
Because bol.com shows a buy box rather than a seller list. See above.

**Does Belgium work the same as the Netherlands?**
Both are accepted by the endpoint. Belgian coverage is thinner in practice, so
expect more `queued_for_scrape` rows on a first pass.

# Actor input Schema

## `eans` (type: `array`):

One or more EAN/GTIN codes. bol.com is looked up by EAN only; bpid, sku and product\_id are not accepted by the endpoint.

## `country` (type: `string`):

Which marketplace to read. Note the endpoint's own codes: the United Kingdom is 'uk', not 'gb'.

## `max_age_hours` (type: `integer`):

How old a cached result may be and still count as a hit. Older than this and a fresh scrape is queued.

## `wait_for_scrape_seconds` (type: `integer`):

An EAN nobody has asked for before is scraped on demand, which takes about five minutes. The run polls for that long before giving up on it. Set 0 to return immediately and collect queued EANs on a later run.

## `force` (type: `boolean`):

Demand data no older than the TTL below. Fires an immediate re-scrape when the cache is staler, and returns the stale row flagged as refreshing so you can poll for the fresh copy.

## `ttl_hours` (type: `integer`):

Only used with Force fresh data. Ignored otherwise.

## `concurrency` (type: `integer`):

How many identifiers to look up at once.

## Actor input object example

```json
{
  "eans": [
    "5702017814674"
  ],
  "country": "nl",
  "max_age_hours": 24,
  "wait_for_scrape_seconds": 600,
  "force": false,
  "ttl_hours": 6,
  "concurrency": 10
}
```

# Actor output Schema

## `offers` (type: `string`):

One row per identifier, with its offers.

## `summary` (type: `string`):

Counts of billable versus free queued rows.

## `errors` (type: `string`):

Failures with a code and a redacted message.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "eans": [
        "5702017814674",
        "5702017814650"
    ],
    "country": "nl",
    "max_age_hours": 24,
    "wait_for_scrape_seconds": 600,
    "ttl_hours": 6,
    "concurrency": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("s-r/bol-offers").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "eans": [
        "5702017814674",
        "5702017814650",
    ],
    "country": "nl",
    "max_age_hours": 24,
    "wait_for_scrape_seconds": 600,
    "ttl_hours": 6,
    "concurrency": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("s-r/bol-offers").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "eans": [
    "5702017814674",
    "5702017814650"
  ],
  "country": "nl",
  "max_age_hours": 24,
  "wait_for_scrape_seconds": 600,
  "ttl_hours": 6,
  "concurrency": 10
}' |
apify call s-r/bol-offers --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,s-r/bol-offers"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/LfZnvV6LJe6dLhZNd/builds/zzoNdPcuHT9IJ6PdA/openapi.json
