# Bol EAN/GTIN Price & Seller Offers API (`pricepirate/bol-ean-gtin-price-data-api`) Actor

Bol seller offers by barcode or numeric product ID in NL and BE. Hard maximum 1000 queries per run; unknown shipping and totals remain null.

- **URL**: https://apify.com/pricepirate/bol-ean-gtin-price-data-api.md
- **Developed by:** [PricePirate](https://apify.com/pricepirate) (community)
- **Categories:** E-commerce, Developer tools, Automation
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.80 / 1,000 results

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 Price & Seller Offers API

Get every seller offer on Bol.com, the leading online marketplace in the
Netherlands and Belgium, through a single Actor. Send a barcode (GTIN/EAN) or
Bol product ID and get the full list of sellers with item price, shipping,
total, currency and condition, all in one JSON structure. Up to 1,000 lookups
per run, no proxies or scraping setup required.

### 🚀 What's new in 1.1

This release moves Bol onto our new bulk lookup infrastructure, the same
backend behind our Google Shopping Actor:

- **20× bigger batches:** up to **1,000 values per run**, submitted as one
  bulk lookup job. Account capacity for Bol grows from 50 to **1,000
  outstanding lookups**.
- **Every seller, new and used:** each `found` result lists all sellers
  returned for the product, in Bol's order, without ranking or condition
  filtering.

Perfect for price-comparison sites, repricing and margin tools,
price monitoring
and dynamic pricing
engines, Bol sellers watching competing offers, deal apps,
dropshipping and product research, and AI shopping assistants targeting the
Dutch and Belgian markets.

### ✓ 2 Ways to Look Up Bol Data

Choose whichever identifier you already have:

| Operation | Value format | Returns |
|---|---|---|
| `search-by-gtin` | GTIN / EAN / UPC barcode with 8, 12, 13 or 14 digits and a valid check digit | Product with all returned seller offers |
| `search-by-id` | Bol product ID, 10–25 digits, no leading zero | Product with all returned seller offers |

### ✓ 2-Country Coverage

Query localized offers on both Bol marketplaces:

Netherlands (`nl`, default) and Belgium (`be`).

### ✓ Multi-Seller Offer Data

A `found` result contains everything the lookup returns for the product:

- **Product:** title, product URL, Bol product ID, EAN, brand and images,
  when available
- **Price summary:** lowest, highest and average **item price**, plus the
  number of returned offers
- **Per-seller offers:** seller name, condition, currency, item price,
  shipping cost, total price and position in Bol's list

> **Note:** Bol does not always state a seller's shipping cost, so the price
> summary uses item prices rather than totals. Compare `total` per offer when
> you need total price
> (price + shipping), and treat `null` as unknown, not free.

### ✓ No Scraping Headaches

We run the lookup infrastructure, so you get structured data without
managing proxies or headless browsers. You do not need
to poll anything either: start the Actor, wait for it to finish and read the
results straight from the dataset.

### 💻 Input

```json
{
  "operation": "search-by-gtin",
  "values": ["4006381333931", "8710103917854"],
  "country": "nl"
}
```

- **`operation`** (required): `search-by-gtin` or `search-by-id`. All values
  in a run share the same operation. The Apify input form defaults to
  `search-by-gtin`; local runs must set it explicitly.
- **`values`** (required): 1–1,000 strings, each one lookup. The Actor trims
  whitespace, drops blank entries and removes duplicates, preserving
  first-seen order. At least one nonblank value must remain. Run the Actor
  again for larger lists.
- **`country`** (optional, default `nl`): `nl` or `be`.

The 1,000 limit applies to the raw list, duplicates included: both the input
form and the Actor reject longer lists before any lookup starts.

Barcodes need a valid check digit and cannot be repeated-digit placeholders
such as `0000000000000`. A 14-digit GTIN must start with `0`, because Bol does
not list case-level GTIN-14 codes. A malformed value becomes its own free
`invalid_input` row; the valid values in the same run are still looked up.

### 💻 Output

One dataset item per trimmed, deduplicated value, in input order. This
illustrative example shows one seller with unknown shipping and one with
shipping included; it is not a live price quote:

```json
{
  "query": "4006381333931",
  "operation": "search-by-gtin",
  "country": "nl",
  "status": "found",
  "result": {
    "id": "9300000123456789",
    "page_type": "product",
    "name": "Example Product Name",
    "url": "https://www.bol.com/nl/nl/p/example-product-name/9300000123456789/",
    "ean": "4006381333931",
    "brand": "Example Brand",
    "description": null,
    "image_urls": ["https://media.s-bol.com/example/product.jpg"],
    "review_rating": null,
    "review_count": null,
    "categories": null,
    "category_ids": null,
    "price_min": 22.49,
    "price_avg": 23.74,
    "price_max": 24.99,
    "offers_count": 2,
    "platform_data": {
      "op": "offers",
      "country": "nl",
      "sku": "9300000123456789",
      "title": "Example Product Name"
    },
    "offers": [
      {
        "sellerId": "Example Store",
        "shop_name": "Example Store",
        "title": "Example Product Name",
        "shop_url": null,
        "shop_type": null,
        "marketplace_name": "bol",
        "shop_review_rating": null,
        "shop_review_count": null,
        "position": "1",
        "condition": "new",
        "currency": "EUR",
        "price": 24.99,
        "shipping": null,
        "total": null,
        "voucher": false,
        "free_return": null,
        "availability_text": null,
        "availability_code": "unknown",
        "classification": "product_page_match",
        "confidence": "high",
        "platform_data": {
          "seller_name": "Example Store",
          "price_value": 24.99,
          "currency": "EUR",
          "shipping_included": false,
          "condition": "new"
        }
      },
      {
        "sellerId": "Second Store",
        "shop_name": "Second Store",
        "title": "Example Product Name",
        "shop_url": null,
        "shop_type": null,
        "marketplace_name": "bol",
        "shop_review_rating": null,
        "shop_review_count": null,
        "position": "2",
        "condition": "used",
        "currency": "EUR",
        "price": 22.49,
        "shipping": 0,
        "total": 22.49,
        "voucher": false,
        "free_return": null,
        "availability_text": null,
        "availability_code": "unknown",
        "classification": "product_page_match",
        "confidence": "high",
        "platform_data": {
          "seller_name": "Second Store",
          "price_value": 22.49,
          "currency": "EUR",
          "shipping_included": true,
          "condition": "used"
        }
      }
    ],
    "source": "bol",
    "country": "nl",
    "fetched_at": "2026-10-09T12:00:00.000Z"
  }
}
```

> **Note:** `price_min`, `price_avg` and `price_max` summarize **item prices**,
> not totals; the average is rounded to two decimals. `shipping` and `total`
> are `null` when Bol does not state the shipping cost. Offer positions are
> one-based strings that preserve Bol's order. Product metadata such as brand,
> images or description can be null or missing. `platform_data` keeps
> Bol-specific fields on both the product and each offer.

Export the dataset as JSON, CSV, Excel or any other Apify format; JSON keeps
the nested seller offers intact. The [dataset schema](.actor/dataset_schema.json)
describes every field and the Overview table. The
[output schema](.actor/output_schema.json) links to the dataset items
endpoint (`{{links.apiDefaultDatasetUrl}}/items`).

### 💻 Statuses & Errors

`status` is `found`, `not_found` or `error`.

`not_found` means Bol returned no matching product or offers for that value in
the selected country. It is not proof that the product never existed. Errors
are never turned into `not_found`.

`not_found` and `error` rows have `result: null`. Error rows also carry a
stable `errorCode` and a sanitized message:

| `errorCode` | Meaning |
|---|---|
| `invalid_input` | The value is not a valid identifier for the selected operation, or the job input was rejected |
| `free_limit_reached` | The value does not fit in the free plan's remaining lifetime allowance |
| `concurrency_limit_reached` | Other runs kept this account's lookup capacity full after all retries |
| `timeout` | The lookup timed out upstream or was unfinished at the Actor's deadline |
| `blocked` | The upstream lookup reported a block |
| `internal_error` | Any other failure or an unusable result |

Error rows are **never charged**. Even an all-error run normally finishes as
`SUCCEEDED` with one row per value telling you what happened, so check the
row statuses, run log and status message rather than the run status alone.

### 💰 Pricing

You pay **per completed query result**: each value that comes back `found` or
`not_found` is charged once, after its row is delivered. Errors and timeouts
are always free.

**Example:** a batch contains 20 queries; 10 come back `found`, 5 come back
`not_found` and 5 time out. You are charged for the 15 completed results; the
5 timeouts are free.

Your run's maximum charge budget limits **billing, not delivery**: every
completed result is still written to the dataset, even after the budget is
used up. A billing failure never withholds data.

**Free Apify plans include 50 lifetime Bol results per account**, across all
runs. This is not 50 per run, it does not reset, and it is separate from our
other marketplaces. `found` and `not_found` results count toward it; errors do
not. Upgrade your Apify plan to remove the cap.

If a batch is larger than your remaining allowance, the Actor looks up the
**first valid values that fit** and returns free `free_limit_reached` rows for
the rest, in input order. On a fresh free account, 1,000 valid values yield up
to 50 lookups and 950 quota rows. Runs started at the same time share the
allowance, so the number that fits can be lower. Failed lookups return their
allowance for later runs, but the current run does not retry its overflow.

### ⏱ Timing & Timeouts

All valid values in a run go into **one bulk lookup job**. The status message
shows live progress, polled every 15 seconds, and all results are written to
the dataset once the job finishes. Results are not streamed one by one, and no
specific throughput is guaranteed.

One shared **890-second** budget covers waiting for capacity and processing.
At the deadline the Actor fetches whatever already finished: those results
still ship, and unfinished values become free `timeout` rows. Set the Actor
timeout to **960 seconds** (the default) to leave room for that final fetch
and delivery.

Each Apify account can have **1,000 Bol lookups in flight at once**, across all
of its runs, in a pool separate from our other marketplaces. When the pool is
full, the Actor waits and retries after 15, 30, 60, 120 and 240 seconds, within
the run's budget. Values that still do not fit become free
`concurrency_limit_reached` rows. To stay under the limit:

- Run full-size batches one after another, not side by side
- Use smaller batches if you need several runs in parallel
- Stagger scheduled runs so they do not all start on the same minute

Avoid restarting a run: a restart submits the input again, which can
duplicate rows, allowance use and charges.

Failed or timed-out requests are **never billed**.

### 📬 Contact & Support

Questions or higher limits: support@pricepirate.com

# Actor input Schema

## `operation` (type: `string`):

Lookup operation for every value.

## `values` (type: `array`):

Maximum 1000 raw entries. Valid barcodes have 8, 12, 13 or zero-prefixed 14 digits and a valid check digit. Bol product IDs have 10–25 digits without a leading zero. Invalid identifiers produce free error rows. Values are trimmed, blanks dropped and duplicates removed.

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

Bol marketplace country.

## Actor input object example

```json
{
  "operation": "search-by-gtin",
  "values": [
    "4006381333931"
  ],
  "country": "nl"
}
```

# Actor output Schema

## `results` (type: `string`):

URL of the default dataset items, not an inline array. One row per normalized value for a valid run input; inspect each row's status, not just the Apify run status.

# 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 = {
    "values": [
        "4006381333931"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("pricepirate/bol-ean-gtin-price-data-api").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 = { "values": ["4006381333931"] }

# Run the Actor and wait for it to finish
run = client.actor("pricepirate/bol-ean-gtin-price-data-api").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 '{
  "values": [
    "4006381333931"
  ]
}' |
apify call pricepirate/bol-ean-gtin-price-data-api --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,pricepirate/bol-ean-gtin-price-data-api"
        }
    }
}
```

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/iYYOm9izOmxtJYcGe/builds/agbK53vJpyvbCGjud/openapi.json
