# Douglas Germany Scraper - Beauty Products, Prices & Reviews (`abotapi/douglas-de`) Actor

Scrape Douglas.de beauty and fragrance products by keyword, category, brand, or URL. Extract brands, current and original prices, discounts, unit prices, size variants, stock, images, ratings, reviews, ingredients, and specifications.

- **URL**: https://apify.com/abotapi/douglas-de.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** E-commerce, Automation, Developer tools
- **Features:** Limited permissions
- **Stats:** 13 total users, 4 monthly users, 97.5% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 product 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

## Douglas DE Scraper: Beauty, Perfumery & Cosmetics Products

Douglas DE Scraper turns douglas.de, Germany's leading beauty, perfumery and cosmetics retailer, into a clean product data API. Get full pricing (current price, crossed-out original price and discount), unit price, availability, image URLs and rating summary, plus a best-effort layer of ingredient lists, how-to-use text, spec tables and per-review text when detail or review enrichment is requested. Search by keyword, browse a category or brand, or paste Douglas links, then export to JSON, CSV or Excel, or pull results straight into your app through the API.

### Why This Scraper?

- **Two ways to find products.** Search by keyword or category, or paste any Douglas product, category or search URL. A brand filter or a pasted brand URL without a keyword or category covers the Parfum department only.
- **Real specials, never guessed.** Douglas has no dedicated sale category; a special is detected from the crossed-out original price, and every product carries an explicit `isOnSpecial` flag plus the exact savings amount and percent.
- **Filters that actually narrow.** Price band and discounted-only filters apply to every mode, including a pasted category, brand or search URL, not just keyword search.
- **Deep product data, best effort.** Turn on ingredient lists (INCI), how-to-use text and the full spec table, or pull rating breakdowns and per-review text; these come from a separate, harder-to-reach route than the listing data and can come back empty on a given run, and you're never charged the enrichment surcharge when they do.
- **A single pasted product is never filtered out.** Paste one product URL and you get that exact product back, whatever its price or discount, though the record is thinner than a listing result (see the FAQ).
- **Unresolvable URLs are named, not guessed.** A pasted link that matches no Douglas category is reported by name and skipped rather than silently swapped for a different department's products.
- **Built for schedules.** Set Max items and Max pages to control run size and cost, and a run that never gets a usable connection fails loudly instead of returning an empty dataset billed as a success.

### Use Cases

- **Price and discount tracking:** monitor current price, was price and savings percent across a brand or category to spot restocks and markdowns.
- **Catalog and assortment analysis:** pull a whole category or brand with availability and images to compare range depth against competitors.
- **Review and rating research:** turn on review enrichment to attempt the rating histogram and per-review text for a product line, best effort.
- **Ingredient and formulation research:** turn on detail enrichment to attempt INCI ingredient lists and spec tables across a category, best effort.
- **Affiliate and content catalogs:** build product feeds with clean pricing and images for a beauty comparison site or newsletter.

### Data You Get

> Sample shape: values are illustrative placeholders, not from a live record.

| Field | Example |
| --- | --- |
| `id` / `sku` | `"5010207032"` / `"1269078"` |
| `ean` | `"4064666325144"` (only with `fetchReviews`, empty otherwise, including for a pasted product URL) |
| `title` | `"TOM FORD Soleil Neige Eau de Parfum"` |
| `brand` | `{name: "TOM FORD", code: null}` (`name` is `null` when the listing carries no brand; `code` is not populated) |
| `url` | `"https://www.douglas.de/de/p/5010207032?variant=1269078"` |
| `price` | `{current: 76.99, original: 115.0, currency: "EUR", discountPercent: 33.1}` (`formattedCurrent`/`formattedOriginal` are not populated) |
| `unitPrice` / `basePriceUnit` | `{amount: 1470.4, currency: "EUR", unit: "per 1 l"}` / `"1 l"` (the price per reference quantity, not per pack; `null` when the listing has none) |
| `recommendedRetailPrice` | `125` (the manufacturer's recommended retail price, `null` when the listing shows none; separate from the strike-through `price.original`) |
| `lowestPrice30d` | `71.41` (lowest price of the last 30 days when the listing shows it, else `null`) |
| `isOnSpecial` / `wasPrice` | `true` / `115.0` (full-price products return `false` / `null`) |
| `savingsAmount` / `savingsPercent` | `38.01` / `33.1` |
| `availability` | `{code: "AVAILABLE", inStock: true}` |
| `images` | `[{url, alt}, ...]` (`alt` is not populated) |
| `rating` | `4.8` |
| `ratingBreakdown` | `[{rating: 5, count: 77}, ...]` (when reviews are fetched) |
| `reviews` | `[{id, author, date, rating, title, body}, ...]` (when reviews are fetched) |
| `ingredients` | `"ALCOHOL, PARFUM / FRAGRANCE, AQUA / WATER / EAU, ..."` (when detail is fetched) |
| `howToUse` | how-to-use instructions (when detail is fetched) |
| `specifications` | `{Duftrichtung: "frisch, blumig", Kopfnote: "Jasmin", ...}` (when detail is fetched) |
| `scrapedAt` | `"2026-09-29T10:15:00+00:00"` |

Every record also carries `currency` (`"EUR"`), `flags` (the store's promotion flags, a list), `contentAmount` and `contentUnit` (pack size, e.g. 50 and `ml`, `null` when unknown), `imageCount`, `thumbnail` (first image URL or `null`), `brandLine`, `productType`, `productFamily`, `promoLabel`, `isMarketplace` and `characteristics`. `variants`, `breadcrumbs`, `description`, `variantLabel` and `reviewCount` are never populated in any mode: the store's listing backend carries no such data, and the product page route reads only the spec table, ingredients and how-to-use text. They are always `[]`, `""` or `null`. `ratingStars` is always `null`; `reviewSummary` is filled only with `fetchReviews`, and only when the store provides one. All nested detail fields are nullable or empty rather than missing, so a non-discounted product, a brandless marketplace listing, a product-URL row or a listing-only run (no ingredients, reviews or spec table fetched) is an explicit negative in the output, never an absent key. A product-URL row has the same keys as a listing row; only `id`, `sku`, `title`, `url`, `scrapedAt`, `specifications`, `ingredients` and `howToUse` can carry values (plus the review fields and `ean` with `fetchReviews`).

### How to Use

1. Pick a **mode**: `search` (keyword, category and/or brand) or `url` (paste Douglas links).
2. Fill in the fields for that mode. The price and discount filters below them apply in both modes.
3. Turn on product detail and/or review enrichment if you need ingredients, spec tables or per-review text.
4. Set **Max items** and **Max pages** to control run size and cost, then click **Start**. Download the dataset as JSON, CSV or Excel, or read it through the API.

**Keyword search:**

```json
{
  "mode": "search",
  "queries": ["vitamin c serum"],
  "gender": "FEMALE",
  "maxItems": 30
}
```

**Browse a brand's discounted products under a price cap:**

```json
{
  "mode": "search",
  "category": "01",
  "brand": "Dior",
  "priceMax": 80,
  "specialsOnly": true
}
```

**Search with detail and review enrichment on:**

```json
{
  "mode": "search",
  "queries": ["eau de parfum"],
  "fetchDetails": true,
  "fetchReviews": true,
  "maxReviewsPerProduct": 50,
  "maxItems": 20
}
```

**Paste a category URL, filtered by price band:**

```json
{
  "mode": "url",
  "urls": ["https://www.douglas.de/de/c/parfum/01"],
  "priceMin": 40,
  "priceMax": 100,
  "maxPages": 3
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/douglas-de").call(run_input={"mode": "search", "queries": ["parfum"], "maxItems": 20})
for product in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(product["title"], product["price"])
```

JavaScript:

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/douglas-de').call({ mode: 'search', queries: ['parfum'], maxItems: 20 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the **Integrations** tab.

#### Filters apply in both modes, with one exception

`priceMin`, `priceMax` and `specialsOnly` narrow the results in search mode and in URL mode alike, including the products found behind a pasted category, brand or search URL. The one exception: a single pasted **product** URL is always returned, whatever its price or discount, because you asked for that exact product.

#### URL mode defaults and product codes

URL mode without `urls` scrapes the four example URLs (one product, one category, one brand, one search), up to `maxItems` products in total. The category, brand and search examples are full listings, so with the default `maxItems` of 100 most of the rows come from the first listing; lower `maxItems` for a smaller sample. Setting `productCodes` and leaving `urls` at those four examples scrapes only your product codes. `productCodes` accepts numeric ids or `/de/p/` URLs. Brand-line URLs (`/de/b/<brand>/<line>/<code>`) are not resolved by the store index and are named in the log and skipped.

#### Unresolved URLs and caps

A pasted URL that douglas.de's own listing backend does not recognize as a category or brand (a content page, a store-finder page, the homepage) is named in the run log and skipped, never replaced with a different department's products. **Max items** stops the run once that many products have been collected (default `100`); **Max pages** caps how many result pages are walked per query, category, brand or URL (one page holds 48 products, default `0` meaning unlimited). With **Max pages** at its default, every page is walked until **Max items** is reached.

#### Recurring monitoring (incremental mode)

Turn on `incrementalMode` and schedule the run. The actor keeps a baseline per search/URL/filter setup (or per `stateKey`). Products that did not change are not returned and are not billed as results; the detail-enrichment charge applies once per returned row that actually received product-page or review data, after that row is delivered, and never for a row that was suppressed as unchanged. Products missing from a run (capped, filtered or partial walks) are left in the baseline and never reported as removed.

### Send results into your apps (MCP connectors)

Optionally pipe the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step **after** the scrape: the Apify dataset is never changed.

**What gets written to the connector:** a condensed, human-readable **summary** of each record, not the full JSON. Each item becomes one entry with a **title** and its key fields flattened to plain text. The **complete record always stays in the Apify dataset**.

1. Authorize a connector once under **Apify → Settings → Integrations** (Notion, Linear, Airtable, or Apify).
2. Select it in the **"Pipe results into your apps"** input field. (If the picker is empty, you haven't authorized a connector yet.)
3. For **Notion**, also set `notionParentPageUrl` to the page where items should be created.

The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave the field empty to skip.

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `search` | `search` (keyword, category, brand) or `url` (paste Douglas links). |
| `queries` | array | no default (prefill: `["parfum"]`) | Keywords to search, e.g. `"vitamin c serum"`, `"parfum"`. Each query runs separately. |
| `category` | string | no default (prefill: `"01"`) | A Douglas category code (e.g. `01` Parfum) or a full category URL. Search mode only. |
| `brand` | string | no default | A brand display name (e.g. `Dior`) or a full brand URL. Search mode only. Without a keyword or category, this covers the Parfum department only; add a category to search a brand elsewhere. |
| `gender` | string | `any` | Restrict to women's, men's or unisex products. Search mode only, no effect in URL mode. |
| `sortBy` | string | `relevance` | Best match, price ascending, price descending, newest first or top rated. Price ordering applies in search mode and to pasted category, brand and search URLs; `newest` and `rating` currently return the same order as best match. |
| `keywords` | array | no default | Alias of `queries`, used when `queries` is empty. |
| `scrapeMode` | string | no default | Alias: `detail` turns `fetchDetails` on. Other values change nothing. |
| `sort` | string | no default | Alias of `sortBy` (`price_low`, `price_high`, `top_rated`). |
| `maxResults` | integer | no default | Alias of `maxItems`, used only while `maxItems` is left at its default of 100; a `maxItems` you changed always wins.
| `promoFlags` | array | no default (prefill: none) | Promotion facet codes such as travel size, new or limited edition. Currently has no effect on results; kept for compatibility. |
| `urls` | array | one example of each shape (product, category, brand, search) | Douglas product, category, brand or search URLs. URL mode only. URL mode without `urls` scrapes these four examples, up to `maxItems` products in total. |
| `productCodes` | array | none | Numeric Douglas product ids or `/de/p/` URLs, scraped like pasted product URLs. URL mode only. Set without `urls`, only these products are scraped. |
| `priceMin` | number | no default | Minimum price in EUR. Applies in both modes; a pasted product URL is always returned regardless. |
| `priceMax` | number | no default | Maximum price in EUR. Applies in both modes; a pasted product URL is always returned regardless. |
| `specialsOnly` | boolean | `false` | Keep only products currently reduced (crossed-out original price). Applies in both modes. |
| `fetchDetails` | boolean | `false` | Open each product page for ingredients (INCI), how-to-use text and the spec table. Off = listing fields only. |
| `fetchReviews` | boolean | `false` | Pull the rating histogram, EAN and per-review text for each product. |
| `maxReviewsPerProduct` | integer | `20` | Cap on reviews returned per product (1-200, hard maximum 200). Ignored unless `fetchReviews` is on. |
| `maxPages` | integer | `0` | Max result pages to walk per query/category/brand/URL (`0` = unlimited). One page holds 48 products. |
| `maxItems` | integer | `100` | Hard cap on the total number of products returned across all targets. |
| `proxyConfiguration` | object | Apify Proxy, residential, Germany | Connection settings, used for a pasted product URL and for detail/review enrichment; a Germany exit is required for those, and changing the country can make them return nothing. Search, category, brand and search-URL runs don't use this setting. |
| `resumeFromRunId` | string | empty | Paste a previous run ID or dataset ID to continue an interrupted pull. Products that run already collected are skipped and not billed again. An unreadable ID stops the run instead of re-collecting everything. |
| `incrementalMode` | boolean | `false` | Recurring monitoring. The first run returns every product as NEW; later runs return only NEW and UPDATED products. Each row carries `changeType`, `changedFields`, `firstSeenAt` and `lastSeenAt`. |
| `stateKey` | string | no default | Name a monitoring campaign to keep its state stable or share it across runs. Empty derives a key from the search/URL/filter settings. |
| `emitUnchanged` | boolean | `false` | Also return products unchanged since the last run, marked UNCHANGED. They are returned and billed like any other result. |
| `ignoreFieldsForChanges` | array | no default | Field names (or dotted names such as `variants.size`) excluded from UPDATED detection. Reviews, review summary, rating histogram, rating, rating stars, review count and `scrapedAt` are never compared. Incremental mode only. |
| `mcpConnectors` | array | no default | Optional: send a summary of each record to apps you authorized under Integrations. |
| `notionParentPageUrl` | string | no default | Notion connector only: page under which records are created. |
| `maxNotifyListings` | integer | `50` | Cap on items written to each connector per run. Does not affect the dataset. |

### Output Example

> Sample shape: values are illustrative placeholders, not from a live record.

```json
{
  "id": "5010207032",
  "sku": "1269078",
  "ean": "4064666325144",
  "title": "TOM FORD Soleil Neige Eau de Parfum",
  "variantLabel": "",
  "url": "https://www.douglas.de/de/p/5010207032?variant=1269078",
  "brand": { "name": "TOM FORD", "code": null },
  "brandLine": "Soleil Neige",
  "productFamily": "Eau de Parfum",
  "productType": "PERFUME",
  "isMarketplace": false,
  "breadcrumbs": [],
  "price": { "current": 76.99, "original": 115.0, "currency": "EUR", "formattedCurrent": null, "formattedOriginal": null, "discountPercent": 33.1 },
  "unitPrice": { "amount": 1539.8, "currency": "EUR", "unit": "per 1 l" },
  "basePriceUnit": "1 l",
  "recommendedRetailPrice": 125,
  "lowestPrice30d": 71.41,
  "isOnSpecial": true,
  "wasPrice": 115.0,
  "savingsAmount": 38.01,
  "savingsPercent": 33.1,
  "promoLabel": "reduziert",
  "availability": { "code": "AVAILABLE", "inStock": true },
  "variants": [],
  "images": [
    { "url": "https://images.douglas.de/is/image/rc/5010207032-01", "alt": null }
  ],
  "description": "",
  "characteristics": ["Eau de Parfum"],
  "rating": 4.8,
  "ratingStars": null,
  "reviewCount": null,
  "ratingBreakdown": [
    { "rating": 5, "count": 77 },
    { "rating": 4, "count": 6 },
    { "rating": 3, "count": 2 },
    { "rating": 2, "count": 1 },
    { "rating": 1, "count": 0 }
  ],
  "reviews": [
    { "id": "1001", "author": "Sample User", "date": "2026-05-01", "rating": 5, "title": "Wonderful scent", "body": "Long lasting and elegant.", "isRecommended": true, "variant": "50 ml", "verified": true }
  ],
  "ingredients": "ALCOHOL, PARFUM / FRAGRANCE, AQUA / WATER / EAU, LIMONENE, LINALOOL",
  "howToUse": "Auf die Haut auftragen, nicht verreiben.",
  "specifications": { "Duftrichtung": "frisch, blumig", "Kopfnote": "Jasmin", "Art-Nr.": "5010207032" },
  "reviewSummary": null,
  "scrapedAt": "2026-09-29T10:15:00+00:00"
}
```

### Plan Requirement

Search, category, brand and search-URL runs read douglas.de's listing backend and need no proxy. A pasted product URL and detail/review enrichment read the product pages, which sit behind an edge access wall that refuses plain datacenter connections; those parts need the default setting (Apify Proxy, residential, Germany), which requires a plan that includes residential proxies. The actor retries on a backup connection when one is refused. If your plan has no proxy access, the product-page parts of a run may come back empty and the run log says so; listing-only runs are unaffected. Changing the country away from Germany can make those parts return nothing.

### FAQ

#### How much does it cost?

You pay per product returned, plus a one-time actor-start charge. The `detail-enrichment` event is charged once per delivered row that actually received product-page or review data, right after that row is pushed, never for a row without that data. In incremental mode, rows suppressed as unchanged are neither returned nor charged. The **Pricing** tab shows the current rates. Use **Max items** and **Max pages** to cap the cost of any run.

#### Is it legal to scrape Douglas?

This actor collects only publicly available product data. You are responsible for how you use it: follow Douglas's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Product prices and specifications are generally facts, but brand names, images and review text may be subject to third-party rights.

#### Can I get only the products I care about on a schedule?

Yes. Schedule the actor from the **Schedules** tab with a fixed keyword, category or brand input, and use `priceMin`/`priceMax`/`specialsOnly` to keep each run scoped to what you track.

#### Why does turning on detail or review enrichment add nothing?

Product detail and reviews are fetched from a separate, harder-to-reach route than the listing data, and that route is currently blocked more often than not; ingredients, how-to-use text, spec tables, the rating breakdown and per-review text can come back empty on most or all products in a given run. The listing fields (price, availability, images, rating summary) are unaffected either way, and the enrichment surcharge is only charged on products where it actually added data, so a run where enrichment adds nothing costs the same as one with it turned off.

#### What does a pasted product URL return?

A thinner record than a search or category result. A pasted product page is fetched through the same route used for detail/review enrichment, so it needs the default German proxy setting and can fail if that route is blocked. When it succeeds without detail or reviews turned on, the row has the same keys as a listing row, but only `id`, `sku`, `title`, `url`, `scrapedAt` and whatever the product page's spec table, ingredients and how-to-use text yield carry values; price, availability, images and rating are empty or `null`. Turn on detail and/or review enrichment on the same run to fill in more, best effort. To get full listing fields (price, availability, images, rating) for a specific product, search or browse for it instead of pasting its URL.

#### Why did my run fail instead of returning an empty dataset?

If the store refuses every connection, or none of the pasted URLs matched a real Douglas category, the run stops with a clear message instead of billing a quiet, empty success. Running again usually succeeds. If only some URLs matched, you still get products for the ones that did, and the others are named in the run log.

#### Can I use it with AI agents or MCP?

Yes. Call it from any Apify integration or MCP client, and use the connector field to push results into Notion, Linear or Airtable.

### 🔗 Want more e-commerce data?

Pair this actor with these related scrapers from the same team:

<table>
<tr><td>🛒 <a href="https://apify.com/abotapi/mediamarkt-de-scraper"><b>MediaMarkt Germany Scraper</b></a><br>Scrape MediaMarkt.de products by keyword or URL. Extract current and original prices...</td><td>🛒 <a href="https://apify.com/abotapi/dm-de-scraper"><b>dm.de</b></a><br>Scrape dm-drogerie markt (dm.de) products: current + strike-through Ausverkauf price with...</td></tr>
<tr><td>🥦 <a href="https://apify.com/abotapi/rewe-de-scraper"><b>REWE.de Scraper</b></a><br>Scrape REWE Germany (rewe.de) grocery products: current price, was-price and discount...</td><td>🛒 <a href="https://apify.com/abotapi/rossmann-de-scraper"><b>ROSSMANN.de Scraper</b></a><br>Scrape ROSSMANN (rossmann.de) drugstore products: current + original strike-through price...</td></tr>
<tr><td>🛒 <a href="https://apify.com/abotapi/obi-de-scraper"><b>OBI.de Scraper</b></a><br>Scrape OBI (obi.de) DIY/home-improvement products: current + strike-through original...</td><td>🛒 <a href="https://apify.com/abotapi/idealo-de-price-comparison-scraper"><b>Idealo.de</b></a><br>Scrape Idealo.de products with identity details, GTIN, price ranges, specifications...</td></tr>
</table>

👉 [Browse all abotapi scrapers](https://apify.com/abotapi)

### 💬 Support

- 🐞 **Found a bug or a missing field?** Open a ticket on the [Issues tab](https://apify.com/abotapi/douglas-de/issues/open). We usually reply within hours.
- 🛠️ **Need another site, extra fields or a private build?** Email <contact@abotapi.com>.
- ⭐ **Enjoying it?** A quick review on the actor page helps other users find it.

# Actor input Schema

## `mode` (type: `string`):

Search mode = keyword + category + brand + filters. URL mode = paste Douglas product, category, brand or search URLs.

## `queries` (type: `array`):

Keywords to search, e.g. 'vitamin c serum', 'parfum', 'dior lipstift'. Each query runs as a separate search.

## `keywords` (type: `array`):

Alias of Search keywords; used only when Search keywords is empty.

## `category` (type: `string`):

A Douglas category code (e.g. '01' Parfum, '03' Make-up, '1205' Gesichtspflege) or a full category URL like https://www.douglas.de/de/c/parfum/01. Leave empty for keyword-only search.

## `brand` (type: `string`):

A Douglas brand code (e.g. 'b0690' Dior) or a full brand URL like https://www.douglas.de/de/b/dior/b0690. Optional.

## `gender` (type: `string`):

Restrict to women's, men's or unisex products. Search mode only: this is sent to the store as a search facet, so it has no effect in URL mode, where a pasted URL carries its own facets.

## `sortBy` (type: `string`):

How to order the results. Applies in search mode and, where the store supports it (price), to pasted category, brand and search URLs. Newest and rating have no store-side sort and keep the store's relevance order.

## `sort` (type: `string`):

Alias of Sort order, used when Sort order is left at relevance. price_low = price-asc, price_high = price-desc, top_rated = rating.

## `promoFlags` (type: `array`):

Site promotion facets to apply (e.g. 'travelSize' travel size, 'computedNewFlag' new, 'computedLimited' limited edition). Optional. Search mode only: these are sent to the store as search facets, so they have no effect in URL mode.

## `urls` (type: `array`):

Full Douglas URLs: product (https://www.douglas.de/de/p/...), category (https://www.douglas.de/de/c/...), brand (https://www.douglas.de/de/b/...) or search (https://www.douglas.de/de/search?q=...). Any other Douglas page (a content page, a store page, the homepage) is looked up against the store's own category index; if it matches no category it is named in the log and skipped, never swapped for a different department's products.

## `productCodes` (type: `array`):

Douglas product codes (numeric ids such as 5010207032) or product URLs (https://www.douglas.de/de/p/...). URL mode only. Each code is scraped like a pasted product URL and is added to the URLs above. If you set product codes and leave the URLs list at its four example URLs, only the product codes are scraped.

## `priceMin` (type: `number`):

Only return products at or above this price. Applies in BOTH search mode and URL mode: in search mode it is also sent to the store as a price facet, and in URL mode it filters the products found behind a pasted category, brand or search URL. A pasted product URL is always returned, whatever its price.

## `priceMax` (type: `number`):

Only return products at or below this price. Applies in BOTH search mode and URL mode: in search mode it is also sent to the store as a price facet, and in URL mode it filters the products found behind a pasted category, brand or search URL. A pasted product URL is always returned, whatever its price.

## `specialsOnly` (type: `boolean`):

Keep only products that are currently reduced (have a strike-through original price). Applies in BOTH search mode and URL mode, including the products found behind a pasted category, brand or search URL; a pasted product URL is always returned whether or not it is reduced. Douglas does not run a dedicated sale category; a 'special' here is any product whose original/list price is crossed out.

## `fetchDetails` (type: `boolean`):

Open each product page for the full spec table, ingredients (INCI) and how-to-use text. Off = listing fields only (faster, cheaper).

## `scrapeMode` (type: `string`):

Alias of Fetch product details: detail turns it on, overview leaves it as set.

## `fetchReviews` (type: `boolean`):

Pull the rating histogram, EAN/GTIN and per-review text (author, date, rating, title, body) for each product.

## `maxReviewsPerProduct` (type: `integer`):

Cap on reviews returned per product (1-200). Ignored unless 'Fetch reviews' is on.

## `maxPages` (type: `integer`):

Maximum result pages to walk per query/category/URL (0 = unlimited, walk until results are exhausted). One page holds 48 products. Leave at 0 for no limit of your own; the actor still stops after 1000 pages per search or URL.

## `maxItems` (type: `integer`):

Hard cap on the total number of products returned across all queries/URLs.

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

Alias of Max products total. Used only while Max products total is left at its default of 100; a Max products total you changed always wins.

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

A Germany exit is required. The preset default is the configuration the store serves; changing it can make a run return no results.

## `resumeFromRunId` (type: `string`):

Paste a previous run ID or dataset ID to continue a large pull without returning or charging for products that run already collected. Use it after an interrupted run. For recurring monitoring of the same search, use Incremental mode below instead.

## `incrementalMode` (type: `boolean`):

Turn on for scheduled monitoring. The first run returns every matching product as NEW. Later runs compare each product with the last run (price, was price, promo label, availability, variants, brand, images, description and any product-page details) and return only NEW and UPDATED products; an UPDATED row's changedFields names exactly what moved. Reviews, ratings, review counts and the scrape time are never compared, since they change without a real product change. Products you do not receive are not charged as results. State is kept separately for each search/URL/filter setup; use State key to name or share a campaign.

## `stateKey` (type: `string`):

Optional. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently configured runs. Leave empty to derive a key automatically from the search/URL and filter settings.

## `emitUnchanged` (type: `boolean`):

Off by default. Turn on to also return products that have not changed since the last run, marked UNCHANGED. These are returned and billed like any other result, so leave it off unless you want the full snapshot every run.

## `ignoreFieldsForChanges` (type: `array`):

Optional. Field names to exclude from UPDATED detection, in addition to the built-in handling (reviews, review summary, rating histogram, rating, rating stars, review count and scrape time never trigger an UPDATED). Use it if another field is noisy in your setup, for example images. A dotted name such as variants.size ignores that key inside every variant while still tracking the rest of each variant.

## `mcpConnectors` (type: `array`):

Optionally send scraped results to apps you already use, via MCP connectors. Authorize a connector once under Apify Settings Integrations, then select it here. A condensed human-readable summary per item is sent; the full record stays in the dataset. Leave empty to skip.

## `notionParentPageUrl` (type: `string`):

URL (or id) of the Notion page under which item pages are created. Required to enable the Notion export; ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on items written to each connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "search",
  "queries": [
    "parfum"
  ],
  "category": "01",
  "gender": "any",
  "sortBy": "relevance",
  "promoFlags": [],
  "urls": [
    "https://www.douglas.de/de/p/5010207032",
    "https://www.douglas.de/de/c/parfum/damenduefte/0101",
    "https://www.douglas.de/de/b/dior/b0690",
    "https://www.douglas.de/de/search?q=hyaluron"
  ],
  "productCodes": [],
  "specialsOnly": false,
  "fetchDetails": false,
  "fetchReviews": false,
  "maxReviewsPerProduct": 20,
  "maxPages": 0,
  "maxItems": 100,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "DE"
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (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 = {
    "mode": "search",
    "queries": [
        "parfum"
    ],
    "category": "01",
    "promoFlags": [],
    "urls": [
        "https://www.douglas.de/de/p/5010207032",
        "https://www.douglas.de/de/c/parfum/damenduefte/0101",
        "https://www.douglas.de/de/b/dior/b0690",
        "https://www.douglas.de/de/search?q=hyaluron"
    ],
    "productCodes": [],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "DE"
    },
    "incrementalMode": false,
    "emitUnchanged": false,
    "ignoreFieldsForChanges": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/douglas-de").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 = {
    "mode": "search",
    "queries": ["parfum"],
    "category": "01",
    "promoFlags": [],
    "urls": [
        "https://www.douglas.de/de/p/5010207032",
        "https://www.douglas.de/de/c/parfum/damenduefte/0101",
        "https://www.douglas.de/de/b/dior/b0690",
        "https://www.douglas.de/de/search?q=hyaluron",
    ],
    "productCodes": [],
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "DE",
    },
    "incrementalMode": False,
    "emitUnchanged": False,
    "ignoreFieldsForChanges": [],
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/douglas-de").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 '{
  "mode": "search",
  "queries": [
    "parfum"
  ],
  "category": "01",
  "promoFlags": [],
  "urls": [
    "https://www.douglas.de/de/p/5010207032",
    "https://www.douglas.de/de/c/parfum/damenduefte/0101",
    "https://www.douglas.de/de/b/dior/b0690",
    "https://www.douglas.de/de/search?q=hyaluron"
  ],
  "productCodes": [],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "DE"
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "ignoreFieldsForChanges": []
}' |
apify call abotapi/douglas-de --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/douglas-de"
        }
    }
}
```

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/adF4wdzFYbIUcCDTs/builds/8bmsUhr02kpyJRCD7/openapi.json
