# Etos Scraper — Dutch Drugstore Products & Prices (`studio-amba/etos-scraper`) Actor

Scrape product listings, prices, promotions, and availability from Etos.nl, the Netherlands' #2 drugstore chain (Ahold Delhaize). Beauty, personal care, health, and household products.

- **URL**: https://apify.com/studio-amba/etos-scraper.md
- **Developed by:** [Studio Amba](https://apify.com/studio-amba) (community)
- **Categories:** E-commerce
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 1,000 result scrapeds

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Etos Scraper -- Dutch Drugstore Products, Prices & Promotions

Scrape product data from Etos.nl -- the Netherlands' #2 drugstore chain, owned by Ahold Delhaize, with over 500 stores. Get structured data on beauty products, personal care, health, baby, and household essentials, including Etos's frequent "2+3 gratis" and percentage-off promotions.

### How to scrape Etos data

Give the scraper a category or brand URL from etos.nl and it returns structured product records: name, brand, price, promotion, stock status, category, and image. No login or cookies required.

#### What is Etos?

Etos is one of the two largest drugstore chains in the Netherlands (alongside Kruidvat), owned by Ahold Delhaize -- the same group behind Albert Heijn. Etos runs on Salesforce Commerce Cloud and stocks thousands of products across beauty, personal care, health, baby, and household categories, with constant promotional cycles ("2+3 GRATIS", percentage discounts, loyalty pricing).

Who uses this scraper:

- **Price comparison platforms** -- Etos is a benchmark for drugstore pricing in the Netherlands, alongside Kruidvat. Track prices to power comparison features across beauty, health, and personal care.
- **Brand managers & manufacturers** -- Monitor how your products are priced and positioned at one of the two biggest Dutch drugstore chains. Detect unauthorized discounting or track promotional cycles.
- **E-commerce competitors** -- If you run a pharmacy, beauty store, or health shop in the Netherlands, Etos is one of your pricing benchmarks. Know when they run promotions before your customers do.
- **Market researchers** -- Analyze the Dutch drugstore market: category sizes, price distributions, brand representation, and seasonal promotional patterns.
- **Deal tracking & couponing** -- Etos's frequent "2+3 gratis" and percentage-off promotions create opportunities for deal-focused content and apps.

#### Browse a category or brand

Grab category or brand URLs from etos.nl navigation:

```json
{
    "categoryUrl": "https://www.etos.nl/haarverzorging/shampoo/",
    "maxResults": 100
}
```

```json
{
    "categoryUrl": "https://www.etos.nl/assortiment/dove/",
    "maxResults": 100
}
```

If `categoryUrl` is provided, it takes priority over `searchQuery`.

#### Search by keyword (best effort)

`searchQuery` matches against a shortlist of known categories and brands (shampoo, conditioner, and roughly two dozen popular brands like Dove, Nivea, CeraVe, L'Oreal). It is **not** a live site-wide search -- etos.nl blocks free-text search query parameters for automated fetches (see Limitations below). For a precise target, always prefer `categoryUrl`.

```json
{
    "searchQuery": "dove",
    "maxResults": 50
}
```

**Tip:** if you leave every field empty, the scraper defaults to the shampoo category so a run always returns data.

### What data does Etos Scraper extract?

| Field | Type | Description |
|-------|------|--------------|
| `name` | String | Full product title |
| `brand` | String | Brand name (Andrelon, Nivea, L'Oreal, Etos eigen merk, etc.) |
| `price` | Number | null | Current selling price in EUR |
| `currency` | String | Always "EUR" |
| `originalPrice` | Number | null | Price before a promotional price cut, when shown |
| `discount` | String | null | Discount percentage computed from price vs originalPrice, e.g. "-40%" |
| `promotion` | String | null | Etos's own promotion label, e.g. "2+3 GRATIS" -- not always a price cut |
| `productId` | String | Etos internal product code |
| `inStock` | Boolean | null | Stock signal; null when the source gives no usable signal (never coerced to false) |
| `category` | String | null | Deepest category in the breadcrumb |
| `categories` | Array | Full category breadcrumb path |
| `imageUrl` | String | Primary product image |
| `url` | String | Full product page URL |
| `scrapedAt` | String | ISO 8601 timestamp |

### Output

A realistic Etos product record:

```json
{
    "name": "Dermolin Anti-roos Shampoo 200 ML",
    "brand": "Dermolin",
    "price": 5.99,
    "originalPrice": 9.99,
    "currency": "EUR",
    "discount": "-40%",
    "promotion": "1 VOOR € 5.99",
    "productId": "120198808",
    "inStock": true,
    "category": "Shampoo",
    "categories": ["Verzorging", "Haarverzorging", "Shampoo"],
    "imageUrl": "https://www.etos.nl/dw/image/v2/BCTN_PRD/on/demandware.static/-/Sites-etos-master-catalog/default/dw3c4bc575/images/120198808/120198808_MCM_PACK_140348.png?sw=230",
    "url": "https://www.etos.nl/producten/dermolin-anti-roos-shampoo-200-ml-120198808.html",
    "scrapedAt": "2026-08-22T10:34:13.046Z"
}
```

### How much does it cost?

Every page fetch goes through the Bright Data Web Unlocker to clear Etos's Akamai bot protection, and each fetch returns roughly 24 products -- a cost estimate of roughly $0.0004-0.0008 per result.

| Scenario | Pages fetched | Estimated cost |
|----------|---------------|-----------------|
| 24 products (one category page) | 1 | ~$0.01 |
| 100 products | ~5 | ~$0.04 |
| 500 products | ~21 | ~$0.20 |
| 2,000 products | ~84 | ~$0.75 |

Your Apify usage cost only settles once a run finishes and shows as SUCCEEDED -- a run that fails partway through still bills for the requests it made.

### Can I integrate?

Connect Etos data to your tools:

- **Google Sheets** -- track Dutch drugstore prices in a spreadsheet
- **Webhooks** -- trigger alerts when promotions start or prices drop
- **Zapier / Make** -- build automated workflows around price changes
- **Slack** -- daily notifications on Etos deals in specific categories
- **Amazon S3 / Google Cloud Storage** -- archive weekly pricing snapshots
- **PostgreSQL / MySQL** -- feed data into your analytics database

### Can I use it as an API?

#### Python

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_API_TOKEN")

run = client.actor("studio-amba/etos-scraper").call(run_input={
    "categoryUrl": "https://www.etos.nl/haarverzorging/shampoo/",
    "maxResults": 100,
})

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    price_info = f"EUR {item['price']}"
    if item.get('originalPrice'):
        price_info += f" (was EUR {item['originalPrice']})"
    print(f"{item['brand']} - {item['name']} -- {price_info}")
```

#### JavaScript

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

const client = new ApifyClient({ token: 'YOUR_API_TOKEN' });

const run = await client.actor('studio-amba/etos-scraper').call({
    categoryUrl: 'https://www.etos.nl/haarverzorging/shampoo/',
    maxResults: 100,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach(item => {
    const sale = item.originalPrice ? ` (was EUR ${item.originalPrice})` : '';
    console.log(`${item.brand} - ${item.name} -- EUR ${item.price}${sale}`);
});
```

### Input parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|--------------|
| `categoryUrl` | String | | An etos.nl category or brand URL to browse, e.g. `https://www.etos.nl/haarverzorging/shampoo/` |
| `searchQuery` | String | `"shampoo"` | Best-effort keyword match against known categories/brands (not a live search) |
| `maxResults` | Integer | `100` | Maximum number of products to return. Hard-capped at 20,000 |
| `inStockOnly` | Boolean | `false` | Off by default. When enabled, products explicitly marked out of stock are excluded |
| `requestDelaySecs` | Integer | `0` | Minimum pause between page requests. Raise for large runs if throttled |
| `proxyConfiguration` | Object | | Left at default -- traffic is routed through Bright Data, not the Apify proxy pool |

### FAQ

**Why can't I search etos.nl for any keyword I want?**
etos.nl's `robots.txt` disallows the query parameters the site's own search and pagination use (`q=`, `start=`, `sz=`, and every filter parameter). The scraper respects that and never requests a disallowed URL, so free-text search is limited to a curated shortlist of common categories and brands. Use `categoryUrl` for anything specific -- copy the URL straight from etos.nl's own navigation.

**How do I get more than 24 products from one category?**
The scraper automatically fans out to related subcategory and brand pages discovered in Etos's own site navigation once the seed category page is exhausted, up to `maxResults`. Set `maxResults` higher to pull from more of these related pages in a single run.

**Can I track promotions like "2+3 gratis"?**
Yes -- the `promotion` field carries Etos's own promotion label exactly as shown on the site. The `discount` field is only populated when the promotion is an actual price cut (a struck-through original price), which "2+3 gratis"-style bundle deals usually are not.

**What language is the data in?**
Product data comes in Dutch, matching what's shown on etos.nl.

**How often do Etos prices change?**
Etos runs regular promotional cycles. Scraping weekly captures new promotion rounds; daily scraping catches flash deals.

**Are Etos's own-brand products included?**
Yes. Etos eigen merk products appear in results like any other brand.

### Limitations

- Free-text search is best-effort only (see FAQ) -- use `categoryUrl` for precise targeting.
- Rating and review data are not exposed on Etos listing pages, so they are not part of the output.
- Stock status reflects the signal exposed on the listing page, not individual store inventory. No explicit "out of stock" example was observed during testing (listing pages appear to only show orderable products) -- `inStock` is `null`, never coerced to `false`, whenever the source doesn't give a clear signal.
- Very large multi-category runs may take longer, since every page fetch routes through the Bright Data Web Unlocker to clear Etos's Akamai bot protection.

### Other retail and beauty scrapers

Build comprehensive Dutch and European drugstore intelligence:

- [Kruidvat Scraper](https://apify.com/studio-amba/kruidvat-scraper) -- Belgium & Netherlands drugstore chain (AS Watson)
- [Douglas.de Scraper](https://apify.com/studio-amba/douglas-de-scraper) -- Germany's #1 perfumery and beauty chain
- [BIPA Scraper](https://apify.com/studio-amba/bipa-scraper) -- Austria's #2 drugstore chain
- [ICI PARIS XL Scraper](https://apify.com/studio-amba/iciparisxl-scraper) -- Belgian beauty and perfume (premium segment)
- [Rituals Scraper](https://apify.com/studio-amba/rituals-scraper) -- Luxury cosmetics and home fragrance
- [Action Scraper](https://apify.com/studio-amba/action-scraper) -- Pan-European discount retailer

### Your feedback

Found an issue or want a new feature? Let us know through the Apify Store actor page. We fix bugs quickly and actively maintain this scraper.

# Actor input Schema

## `categoryUrl` (type: `string`):

An etos.nl category or brand URL to browse, e.g. https://www.etos.nl/haarverzorging/shampoo/ or https://www.etos.nl/assortiment/dove/. Takes priority over searchQuery.

## `searchQuery` (type: `string`):

Dutch keyword or brand name (e.g. 'shampoo', 'dove', 'cerave'). Best-effort: matched against a shortlist of known categories/brands, NOT a live site-wide search (etos.nl blocks free-text search query parameters for automated fetches — see README). Use categoryUrl for a precise target. Ignored if categoryUrl is set.

## `maxResults` (type: `integer`):

Maximum number of products to scrape. Hard-capped at 20,000 per run.

## `inStockOnly` (type: `boolean`):

Off by default. When enabled, products explicitly marked out of stock are excluded (products with unknown stock status are still returned, since 'unknown' is not the same as 'out of stock').

## `requestDelaySecs` (type: `integer`):

Minimum pause between page requests. Raise this (5-15s) for large runs if you hit throttling. 0 = full speed with automatic adaptive backoff.

## `brightDataApiKey` (type: `string`):

Optional. Your own Bright Data Web Unlocker API key. Normally not needed — the actor ships with access. Only set this to route traffic through your own Bright Data account.

## `proxyConfiguration` (type: `object`):

Proxy settings. etos.nl is fetched through the Bright Data Web Unlocker to clear Akamai bot protection, so this can be left at its default.

## Actor input object example

```json
{
  "categoryUrl": "https://www.etos.nl/haarverzorging/shampoo/",
  "searchQuery": "shampoo",
  "maxResults": 20,
  "inStockOnly": false,
  "requestDelaySecs": 0,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

No description

# 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 = {
    "categoryUrl": "https://www.etos.nl/haarverzorging/shampoo/",
    "searchQuery": "shampoo",
    "maxResults": 20,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("studio-amba/etos-scraper").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 = {
    "categoryUrl": "https://www.etos.nl/haarverzorging/shampoo/",
    "searchQuery": "shampoo",
    "maxResults": 20,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("studio-amba/etos-scraper").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 '{
  "categoryUrl": "https://www.etos.nl/haarverzorging/shampoo/",
  "searchQuery": "shampoo",
  "maxResults": 20,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call studio-amba/etos-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,studio-amba/etos-scraper"
        }
    }
}

```

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/ItodtqWTZl24jeDFQ/builds/kcLhPDsIQHiG3EjkD/openapi.json
