# dm.de - Prices, Offers, Unit Price & Reviews (`abotapi/dm-de-scraper`) Actor

Scrape dm-drogerie markt (dm.de) products: current + strike-through Ausverkauf price with discount, per-unit pricing (per 100ml/kg/piece), brand incl. dm own-brands, EAN, ingredients, pack-size variants, and full reviews with rating breakdown. Search or paste links.

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

## Pricing

from $1.00 / 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

## dm.de Scraper: Prices, Discounts, Unit Pricing & Reviews

dm.de Scraper turns dm-drogerie markt (dm.de), Germany's drugstore and personal-care retailer, into a clean product data feed. Search by keyword, by a real dm.de category, or by the site's own currently-reduced Ausverkauf items, or paste product and listing links directly. Every record carries the current price, the strike-through original price with a computed discount when a product is marked down, per-unit pricing (for example unit price by volume or weight) where the site shows it, and a full customer review history with a rating breakdown. Export to JSON, CSV or Excel, pull results straight into your app through the API, or pipe them into Notion, Linear or Airtable.

### Why This Scraper?

- **Was-price and discount, structured, not scraped from a badge.** When a product carries a reduced original price, both prices are captured along with the computed discount amount and percentage.
- **Real per-unit pricing.** Bodycare, food, and household products return their unit price (by volume, weight, or piece) separately from the pack price, exactly as the site itself breaks it down.
- **Ausverkauf, first class.** A `specialsOnly` toggle narrows any search or category listing to dm's own live clearance/sellout feed, the site's own queryable discount signal, not a guessed category.
- **Full review history.** Overall rating, review count, a full 1 to 5 star rating breakdown, and every individual review with author, date, rating, title, and body text.
- **Rich detail on demand.** Full breadcrumb category path, flattened product information (description, ingredients, usage notes, which varies per category and is never hardcoded), pack-size variants with their own price and EAN, brand image, and online/store availability, plus coverage of every brand dm carries, including its own private-label lines alongside national brands.
- **Two ways in, one set of filters.** Keyword or category search, or paste any product/listing link and continue pagination automatically. Brand, rating, price range, Ausverkauf, and sort narrow the results either way.
- **Resume and recurring updates.** Turn on Incremental mode to get only new and updated products on a scheduled re-run of the same search (plus reappeared ones if you also track expiry), see "Resume and recurring updates" below for exactly how that works.

### Use Cases

- **Price and discount tracking:** monitor Ausverkauf and clearance prices, plus per-unit pricing, across brands to catch genuine markdowns.
- **Retail intelligence:** benchmark dm's own private-label and national-brand pricing over time for competitor research.
- **Review and reputation analysis:** study rating breakdowns and review text for personal-care and household products.
- **Catalog and assortment research:** build category-level snapshots of the range, including pack sizes and ingredient lists.
- **Shopping tools and feeds:** feed a price-comparison site, browser extension, or shopping assistant with structured product and pricing data.

### Data You Get

> Sample shape: values below are entirely invented placeholders, not from a live product.

| Field | Example |
| --- | --- |
| `productId` / `sku` | `"2938471"` |
| `ean` | `"4009876543219"` |
| `name` | `"Feuchtigkeitscreme Ringelblume 50 ml"` |
| `brand` | `"GartenPflege"` |
| `category` / `categoryPath` | `"Gesichtspflege"`, `["Pflege & Parfum", "Gesichtspflege", "Cremes & Masken"]` |
| `url` | `"https://www.dm.de/p/d/2938471/feuchtigkeitscreme-ringelblume"` |
| `price` / `currency` | `5.29`, `"EUR"` (HUF, PLN, CZK, RSD, BAM in those markets) |
| `imageUrl` / `scrapedAt` | `imageUrl` is the same value as `image`; `scrapedAt` is the UTC scrape timestamp (never triggers an incremental UPDATED) |
| `originalPrice` / `discountAmount` / `discountPercent` | `6.99`, `1.7`, `24.3` |
| `isOnSpecial` / `promoLabel` | `true`, `"Ausverkauf"` |
| `unitPrice` / `unitPriceBasis` / `packSize` | `10.58`, `"100 ml"`, `"50 ml"` |
| `rating` / `reviewCount` | `4.2`, `63` |
| `onlineAvailable` / `onlineAvailabilityStatus` / `storeAvailabilityStatus` | `true`, `"Lieferbar"`, `"Im Markt vorrätig"` |
| `image` / `images` | `"https://www.dm.de/pics/p2938471.jpg"`, `["https://www.dm.de/pics/p2938471.jpg"]` |
| `description` / `ingredients` | full text fields |
| `productDetails` | `{"Produktmerkmale": "...", "Verwendungshinweise": "..."}` (flattened per category) |
| `variants` | `[{"productId": "2938473", "ean": "4009876543226", "label": "150 ml", "isSelected": false}]` |
| `isPharmacy` | `false` |
| `reviews.averageRating` / `reviews.totalReviewCount` | `4.2`, `63` |
| `reviews.ratingDistribution` | `[{"rating": 5, "count": 36}, {"rating": 4, "count": 14}, {"rating": 3, "count": 6}, {"rating": 2, "count": 3}, {"rating": 1, "count": 4}]` |
| `reviews.items[]` | `[{"rating": 5, "title": "Fictional title text", "body": "Fictional review body text.", "author": "Fictional Reviewer", "date": "2026-03-05T10:15:00.000Z", "isRecommended": true, "verifiedPurchaser": true}]` |

Any record carries `priceExclVat` (the VAT-exclusive price) when the site shows a net price. Detail-enriched records also carry `brandImage` and `categoryCodes` when the site provides them. When `incrementalMode` is on, every returned record also carries `changeType` (`NEW`, `UPDATED`, `UNCHANGED`, `REAPPEARED`, or `EXPIRED`), `changedFields` (non-empty only for `UPDATED`), `firstSeenAt`, and `lastSeenAt`, see "Resume and recurring updates" below for exactly when each value is produced.

### How to Use

1. Pick a **mode**: `search` (keyword, category, and/or Ausverkauf) or `url` (paste product or listing links).
2. Fill in the fields for that mode, and set the shared filters, brand, rating, price range, Ausverkauf, and sort, that apply to both modes.
3. Turn on **Fetch full product detail** and/or **Fetch reviews** for full detail and review history, then set **Max products total** to control run size and cost.
4. Click **Start**, then download the dataset as JSON, CSV, or Excel, or read it through the API.

**Keyword search, first page, with reviews (default):**

```json
{
  "mode": "search",
  "searchTerm": "bodylotion",
  "maxItems": 20
}
```

**Category browse, filtered to a brand and only currently-reduced Ausverkauf items:**

```json
{
  "mode": "search",
  "searchTerm": "pflege-und-parfum/koerperpflege/bodylotion-und-hautcreme",
  "brands": ["Balea"],
  "specialsOnly": true,
  "sortBy": "PRICE_ASC",
  "maxItems": 50,
  "maxPages": 5
}
```

#### URL shapes (url mode)

| Shape | Example |
|---|---|
| Search page | `https://www.dm.de/search?query=shampoo` |
| Search page, paginated (`currentPage`, 0-indexed) | `https://www.dm.de/search?query=shampoo&currentPage=1` |
| Top-level category page | `https://www.dm.de/haare` |
| Category page, paginated (`currentPage`, 0-indexed; page 2 of the category) | `https://www.dm.de/haare?currentPage=1` |
| Nested category page | `https://www.dm.de/pflege-und-parfum/koerperpflege/bodylotion-und-hautcreme` |
| Ausverkauf (clearance) page | `https://www.dm.de/ausverkauf` |
| Product detail page | `https://www.dm.de/p/d/1493055/head-und-shoulders-shampoo-anti-schuppen-classic-clean` |
| Product detail page, article number only (no slug) | `https://www.dm.de/p/d/1493055` |
| Product detail page on the bare host (redirects to www) | `https://dm.de/p/d/1493055` |

Behaviour change: in url mode, an omitted URLs field (API call or schedule) now scrapes the six built-in examples (search, paginated search, top-level category, nested category, Ausverkauf and product with slug), previously a single search link. An untouched Console field runs the nine prefilled examples, which are those six plus the paginated category, the article-number-only product and the bare-host product. The paginated category is page 2 of `/haare`, so it returns different products, while the two extra product links resolve to a product already in the list and are deduplicated. Max products total is shared across all of them either way. The six built-in examples are not part of the incremental state key, so existing scheduled campaigns keep their saved baseline. The nine prefilled Console examples are not excluded, so an untouched-Console run keeps its own baseline; a non-German `country` is part of the key, so each market keeps its own baseline. An unknown category path or an article number that does not exist logs a warning for that URL and is skipped, it never produces an empty row.

**Full product detail and reviews for a pasted product link:**

```json
{
  "mode": "url",
  "urls": ["https://www.dm.de/p/d/<product-id>/<product-slug>"],
  "fetchDetails": true,
  "fetchReviews": true,
  "maxReviewsPerProduct": 0
}
```

**Paste a listing link and keep paginating forward:**

```json
{
  "mode": "url",
  "urls": ["https://www.dm.de/search?query=bodylotion"],
  "maxItems": 100,
  "maxPages": 10
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/dm-de-scraper").call(run_input={"mode": "search", "searchTerm": "bodylotion", "maxItems": 20})
for product in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(product["name"], product["price"], product["currency"])
```

JavaScript:

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/dm-de-scraper').call({ mode: 'search', searchTerm: 'bodylotion', 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.

#### Resume and recurring updates

There are two different things here. **Resume** (`resumeFromRunId`) continues one specific interrupted or previous large crawl: paste a run id or dataset id and this run skips products already collected there, returning only the remaining new products. **Incremental mode** (`incrementalMode`) is for running the same search again and again on a schedule, for example daily: the actor remembers the previous run of that search by itself, so you never paste a run id.

The first run for a search returns everything as `NEW`. Later runs return only `NEW`, `UPDATED`, and `REAPPEARED` products by default, duplicates and unchanged products are suppressed and not billed. `REAPPEARED` only appears for a product that a previous run had already marked `EXPIRED`, and that marking itself only happens once `emitExpired` was on and a run completed a full, uncapped, non-resumed scan of the same search, and the product has now been seen again. A product is `UPDATED` when any field other than `reviews`, `rating`, `reviewCount` and `scrapedAt` changes (price, original price, discount, special flag, availability, variants, text). Those four never mark a product `UPDATED` on their own; they are still returned on every record. Use `ignoreFieldsForChanges` to exclude more fields (a field name matches at any depth, a dotted path such as `variants.price` targets one nested place); ignored fields stay in the output and are left out of `changedFields`. Turn on `emitUnchanged` or `emitExpired` only when you also want those rows returned, and billed for. Set `stateKey` to name or deliberately share a monitoring campaign, or leave it empty to let the actor derive one automatically from the search and detail/review settings. Combining `resumeFromRunId` with `incrementalMode` on a brand-new state key bootstraps the incremental baseline from that resume dataset instead of starting empty.

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

Optionally pipe results into the apps you already use through Model Context Protocol (MCP) connectors. Authorize a connector once under Apify, Settings, Integrations, then select it in the `mcpConnectors` field. Each connector receives a condensed, human-readable summary per product, name plus key fields, while the complete record always stays in the Apify dataset. For Notion, set `notionParentPageUrl` to the page the item pages should be created under, and use `maxNotifyListings` to cap how many items are exported per connector per run. Supported connectors: Notion, Linear, Airtable, and Apify. Leave the field empty to skip, it never changes the dataset output.

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `search` | `search` (keyword, category, and/or Ausverkauf) or `url` (paste product or listing links). |
| `searchTerm` | string | none (prefill `shampoo`) | Keyword or a real dm.de category path (search mode only). |
| `country` | string | `de` | dm market: `de`, `at`, `hu`, `pl`, `sk`, `hr`, `si`, `bg`, `rs`, `ba`, `cz`. Keyword search, product URLs, Ausverkauf, filters, sorting and details work in every market; category paths and per-review rows (the listing rating and count are kept) are Germany-only. Per-unit price is read whenever the market's product tile prints one (connectors "je", "za", the Cyrillic "за" and "/" are understood, including space-grouped thousands such as "2 499,00 Ft"), and stays empty when the tile shows none. An unsupported value ends the run with a clear error. |
| `searchQuery` | string | none | Alias of `searchTerm`; when set it is used instead. |
| `categoryUrl` | string | none | Alias of `urls` for a single URL; when set and no keyword is given it switches the run to URL mode and is used instead of `urls`. |
| `maxResults` | integer | none | Alias of `maxItems`; when set it is used instead. |
| `urls` | array | one example per URL shape (see table above) | Product or listing URLs to scrape (url mode only). |
| `specialsOnly` | boolean | `false` | Keep only products currently in dm's Ausverkauf/clearance feed (applies to both search and url mode; has no effect on a single pasted product URL, there is no listing to narrow). |
| `brands` | array | none (prefill empty) | Exact brand names to keep (applies to both search and url mode, including a single pasted product URL). |
| `minRating` | integer | none | 1 to 5, minimum rating to keep (applies to both search and url mode, including a single pasted product URL). |
| `minPrice` | number | none | Only return products priced at or above this amount, in euros. |
| `maxPrice` | number | none | Only return products priced at or below this amount, in euros. |
| `sortBy` | string | `RELEVANCE` | `RELEVANCE`, `PRICE_ASC`, `PRICE_DESC`, `NEWEST`, or `RATING` (applies to both search and url mode; has no effect on a single pasted product URL, there is no listing to reorder). |
| `fetchDetails` | boolean | `false` | Fetch full product detail (breadcrumb, description, ingredients, pack-size variants, brand image, availability). A pasted product URL always fetches detail regardless of this toggle, since a bare product page has no listing card to fall back on, and is charged the enrichment surcharge when the detail or review fetch succeeds. |
| `fetchReviews` | boolean | `true` | Fetch full review history. Shares one enrichment surcharge with `fetchDetails`, charged at most once per product, and only when at least one of the two fetches actually succeeds. |
| `maxReviewsPerProduct` | integer | `20` | Cap on reviews per product (maximum 1000); `0` = all, up to that maximum. |
| `maxPages` | integer | `0` | Cap on result pages per search/category/URL entry; `0` = unlimited, the run stops at Max products total instead. |
| `maxItems` | integer | `20` | Cap on total products returned; `0` = unlimited. |
| `resumeFromRunId` | string | none | Continue one specific previous run/dataset: products already collected there are skipped, so this run only returns new products. For recurring monitoring of the same search, use Incremental mode instead, see "Resume and recurring updates" above. |
| `incrementalMode` | boolean | `false` | Daily/recurring monitoring of this same search. First run returns everything as `NEW`; later runs return only `NEW`/`UPDATED` by default (`REAPPEARED` needs `emitExpired` too, plus a prior full, uncapped scan). See "Resume and recurring updates" above. |
| `stateKey` | string | none | Incremental mode only. Name this monitoring campaign, or deliberately share state across differently-configured runs. Leave empty to derive one automatically. |
| `emitUnchanged` | boolean | `false` | Incremental mode only. Also return products unchanged since the last run, marked `UNCHANGED`. Adds and bills extra rows you already have. |
| `emitExpired` | boolean | `false` | Incremental mode only. Also return products from a previous run no longer found, marked `EXPIRED`, once a run has fully scanned the search, not capped, not a resume. Adds and bills extra synthetic rows. |
| `ignoreFieldsForChanges` | array | none | Incremental mode only. Field names (or dotted paths such as `variants.price`) that must not make a product `UPDATED`. They stay in the output but are left out of change detection and `changedFields`. `reviews`, `rating`, `reviewCount` and `scrapedAt` are always ignored. |
| `proxy` | object | Apify Proxy (`useApifyProxy: true`) | Connection settings. |
| `mcpConnectors` | array | none | Optional MCP connectors to export results into (Notion, Linear, Airtable, Apify). |
| `notionParentPageUrl` | string | none | Notion connector only: page under which item pages are created. |
| `maxNotifyListings` | integer | `50` | Cap on items exported to each connector per run. Does not affect the dataset. |

### Output Example

> Sample shape: values below are entirely invented placeholders, not from a live product.

```json
{
  "productId": "2938471",
  "sku": "2938471",
  "ean": "4009876543219",
  "name": "Feuchtigkeitscreme Ringelblume 50 ml",
  "brand": "GartenPflege",
  "category": "Gesichtspflege",
  "url": "https://www.dm.de/p/d/2938471/feuchtigkeitscreme-ringelblume",
  "price": 5.29,
  "currency": "EUR",
  "originalPrice": 6.99,
  "discountAmount": 1.7,
  "discountPercent": 24.3,
  "isOnSpecial": true,
  "promoLabel": "Ausverkauf",
  "unitPrice": 10.58,
  "unitPriceBasis": "100 ml",
  "packSize": "50 ml",
  "rating": 4.2,
  "reviewCount": 63,
  "onlineAvailable": true,
  "onlineAvailabilityStatus": "Lieferbar",
  "storeAvailabilityStatus": "Im Markt vorrätig",
  "isPharmacy": false,
  "priceExclVat": 4.94,
  "image": "https://www.dm.de/pics/p2938471.jpg",
  "images": ["https://www.dm.de/pics/p2938471.jpg"],
  "reviews": {
    "averageRating": 4.2,
    "totalReviewCount": 63,
    "ratingDistribution": [
      { "rating": 5, "count": 36 },
      { "rating": 4, "count": 14 },
      { "rating": 3, "count": 6 },
      { "rating": 2, "count": 3 },
      { "rating": 1, "count": 4 }
    ],
    "items": [
      {
        "rating": 5,
        "title": "Fictional title text",
        "body": "Fictional review body text.",
        "author": "Fictional Reviewer",
        "date": "2026-03-05T10:15:00.000Z",
        "isRecommended": true,
        "verifiedPurchaser": true
      }
    ]
  }
}
```

### Plan Requirement

A datacenter connection is enough: the default Apify Proxy reads dm.de without a residential proxy, and residential is optional (it only gives more headroom on very large or frequent runs). Under heavy bursts dm.de rate-limits its search service; the actor backs off and takes a fresh connection before giving up.

Apify free plans do not include residential proxies. If the connection you selected is unavailable, the run logs "The selected connection is unavailable to this run. Apify free plans do not include residential proxies. Continuing on a backup connection; some pages may still be unavailable." and continues, then logs "Served over the backup connection (the default connection was unavailable)." when the backup served the data. If the proxy refuses the account entirely, the run fails with "The proxy connection was refused for this account, so no page could be read." and a paid plan is needed. If the backup also cannot load the pages, it fails with "No results: residential proxies are unavailable on this run ... and the backup connection could not load the requested dm.de pages." If every results request fails, it fails with "Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search."

#### Output views

The dataset has four views: **Products** (listing overview), **Reviews (nested)**, **Product details** and **Changes (incremental)**. The review cap per product is `maxReviewsPerProduct`, at most 1000; `0` or any larger value is treated as 1000.

### FAQ

#### How much does it cost?

You pay per product returned, plus one detail-enrichment charge per product (unit: one product with successfully fetched detail or reviews, charged at most once per product), and only when that product's full detail or review history is actually fetched successfully. `fetchDetails` and `fetchReviews` share that single surcharge, charged at most once per product. The **Pricing** tab shows current rates. Use **Max products total** to cap the cost of any run.

#### Is it legal to scrape dm.de?

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

#### Can I get only new or changed products on a schedule?

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental mode**. Each run then returns only new and updated products, and unchanged ones are not billed unless you turn on `emitUnchanged`.

#### What's the difference between Resume and Incremental mode?

**Resume** continues one specific interrupted run using a pasted run or dataset id. **Incremental mode** is for running the same search again and again, for example daily, without ever pasting an id: the actor remembers the previous run of that search by itself.

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

If Apify Proxy refuses this account the connection the run asked for (for example, a proxy group your plan doesn't include), the run fails with a clear connection message instead of reporting "no products matched". If dm.de itself returns no results or blocks individual requests, the run still succeeds with whatever it collected, which can be an empty dataset. Check that your plan includes the proxy access the run requests, then try again.

#### 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/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/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/hornbach-de-scraper"><b>HORNBACH Products, Prices &amp; Reviews Scraper</b></a><br>Scrape HORNBACH (hornbach.de) DIY, building &amp; garden products: current + strike-through...</td><td>🥦 <a href="https://apify.com/abotapi/kaufland-de-scraper"><b>Kaufland.de Scraper</b></a><br>Scrape Kaufland.de, Germany's hypermarket and online marketplace: keyword/category/brand...</td></tr>
<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/thalia-de-scraper"><b>Thalia.de Scraper</b></a><br>Scrape Thalia.de books, eBooks, audiobooks, toys and stationery. Search by keyword or...</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/dm-de-scraper/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' finds products by keyword, by a real dm.de category (e.g. 'haare' or 'pflege-und-parfum/koerperpflege/bodylotion-und-hautcreme'), and/or the site's own currently-reduced Ausverkauf items, with real site filters. 'url' scrapes any product or category/listing page URL you paste, walking pagination forward automatically.

## `searchTerm` (type: `string`):

Free-text keyword (e.g. 'shampoo') or a real dm.de category path exactly as it appears in the site's own navigation or URLs (e.g. 'haare' or 'pflege-und-parfum/koerperpflege/bodylotion-und-hautcreme'). Leave empty and enable Ausverkauf only below to browse the whole clearance feed.

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

Alias of Search keyword or category. When set, it is used instead of the field above.

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

Paste dm.de product pages (.../p/d/<article number>/<slug>), category pages (e.g. .../haare), the Ausverkauf page (.../ausverkauf) or search pages (.../search?query=shampoo). Pagination continues forward automatically from any ?currentPage= already in the URL. Omitting this field in URL mode (API or schedule) scrapes six built-in examples, one per main shape; leaving it untouched in the Console runs the nine prefilled examples (the six plus a paginated category, an article-number-only product link and a bare-host product link). Max products total is shared across them.

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

Alias of URLs to scrape for a single dm category, product or search page URL. When set (and no keyword is given) it switches the run to URL mode and is used instead of the list above.

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

dm market to scrape. Germany (de) has the full feature set. For the other markets keyword search, product URLs, Ausverkauf, price/brand/rating filters, sorting and details work; category paths and per-review rows (reviews fall back to the rating and count) are Germany-only. Prices come back in the market's currency; unitPrice is filled whenever the market's product tile prints a unit price.

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

dm.de's real, queryable promo taxonomy is its 'Ausverkauf' (clearance/sellout) feed -- verified live. There is no separate queryable 'Angebote'/'Aktion' product feed: the site's 'Aktionen' nav entry is an editorial content hub with no product API behind it, so it is intentionally not exposed here. Turning this on narrows to Ausverkauf items (optionally combined with the keyword/category above), each carrying the site's own strike-through original price and discount.

## `brands` (type: `array`):

Only return products from these brands, e.g. 'Balea', 'dmBio', 'alverde NATURKOSMETIK', 'Nivea'. Match the exact brand name as shown on the site (includes dm's own private-label brands). Leave empty for all brands.

## `minRating` (type: `integer`):

Only return products rated at or above this many stars, 1-5.

## `minPrice` (type: `number`):

Only return products priced at or above this amount, in euros.

## `maxPrice` (type: `number`):

Only return products priced at or below this amount, in euros.

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

dm.de's own sort order, verified live to reorder the full server-side result set (so it stays correct across pagination, not just within one fetched page).

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

A pasted product link always fetches full detail regardless of this toggle, and is charged the enrichment surcharge either way — it has no listing card to fall back on, so without the detail fetch the record would be almost empty. Everywhere else, this toggle adds the full breadcrumb category path, category codes, description, ingredients (Inhaltsstoffe), all other product characteristics (flattened, varies per category), pack-size variants with their own price/EAN, brand image, and online/store availability from the product detail API. Current price, was-price/discount, unit price and rating are already included without this toggle. This toggle and 'Fetch reviews' share one enrichment surcharge, charged at most once per product.

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

Fetch each product's full review history: overall rating, review count, a full 1-5 rating breakdown, and per-review author/date/rating/title/body. Products with no reviews simply return an empty list. This toggle and 'Fetch full product detail' share one enrichment surcharge, charged at most once per product no matter how many reviews are fetched.

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

Cap on reviews fetched per product when 'Fetch reviews' is on. 0 = all available reviews, up to 1000.

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

Stop after this many result pages per search/category/URL entry. 0 (default) = unlimited -- the run stops at Max products total instead.

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

Hard cap on total products returned across every search/category/URL entry. 0 = unlimited (still bounded by Max pages per search); values above 1,000,000 are rejected.

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

Alias of Max products total for users coming from other dm scrapers. When set, it is used instead of Max products total. 0 = unlimited.

## `proxy` (type: `object`):

The default connection works on every Apify plan, including the free tier. A residential connection is optional and only needed for very large or sustained runs.

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

Paste a previous run id (or its dataset id) to continue that run: products already collected there are skipped, so this run only returns new products (a delta). Leave empty for a normal run. For recurring daily/weekly monitoring of the SAME search instead, use Incremental changes below.

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

Turn this on for daily or recurring monitoring. The first run returns all matching products as NEW. Later runs return only NEW, UPDATED and REAPPEARED products by default. A product is UPDATED when any field other than reviews, rating, reviewCount and scrapedAt changes (price, original price, discount, special flag, availability, variants, text and so on); those four are still returned on every record but never trigger UPDATED, and you can exclude more with "Ignore fields when detecting changes". Turn on "Emit unchanged products" or "Emit expired products" only when you also want those products returned (and billed). State is kept separately per search/category/URL and per Fetch full product detail/Fetch reviews setup; use State key to name or deliberately share a monitoring campaign. To continue one specific interrupted run instead, use Resume from a previous run above.

## `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 let the actor derive a key automatically from the search/category/URL and Fetch full product detail/Fetch reviews settings -- different searches then never mix state with each other.

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

Off by default. Turn on to also return products that have not changed since the last run, marked UNCHANGED. This returns -- and bills -- extra rows you already have, so leave it off unless you specifically want the full snapshot every run.

## `emitExpired` (type: `boolean`):

Off by default. Turn on to also return products that were present in a previous run but are no longer found, marked EXPIRED. Only produced once a run has fully scanned the tracked search/URL set -- not when Max products/Max pages capped it, or when Resume from a previous run was used. This returns -- and bills -- extra synthetic rows, so leave it off unless you need expiry tracking.

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

Field names that must NOT make a product UPDATED, for example priceExclVat. Matches the field at any depth; use a dotted path such as variants.price to target one nested place. Ignored fields are still returned in the output, they are only left out of change detection and changedFields. Reviews, rating, reviewCount and scrapedAt are always ignored.

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

Optionally send the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize a connector once under Apify -> Settings -> Integrations, then select it here. The connector receives a condensed, human-readable summary per item (title + key fields), not the full JSON; the complete record stays in the dataset. Leave empty to skip. Supported: Notion (https://mcp.notion.com/mcp), Linear (https://mcp.linear.app/sse), Airtable (https://mcp.airtable.com/mcp), Apify (https://mcp.apify.com).

## `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",
  "searchTerm": "shampoo",
  "urls": [
    "https://www.dm.de/search?query=shampoo",
    "https://www.dm.de/search?query=shampoo&currentPage=1",
    "https://www.dm.de/haare",
    "https://www.dm.de/haare?currentPage=1",
    "https://www.dm.de/pflege-und-parfum/koerperpflege/bodylotion-und-hautcreme",
    "https://www.dm.de/ausverkauf",
    "https://www.dm.de/p/d/1493055/head-und-shoulders-shampoo-anti-schuppen-classic-clean",
    "https://www.dm.de/p/d/1493055",
    "https://dm.de/p/d/1493055"
  ],
  "country": "de",
  "specialsOnly": false,
  "brands": [],
  "sortBy": "RELEVANCE",
  "fetchDetails": false,
  "fetchReviews": true,
  "maxReviewsPerProduct": 20,
  "maxPages": 0,
  "maxItems": 20,
  "proxy": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `detail` (type: `string`):

No description

## `reviews` (type: `string`):

No description

## `changes` (type: `string`):

No description

## `summary` (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",
    "searchTerm": "shampoo",
    "urls": [
        "https://www.dm.de/search?query=shampoo",
        "https://www.dm.de/search?query=shampoo&currentPage=1",
        "https://www.dm.de/haare",
        "https://www.dm.de/haare?currentPage=1",
        "https://www.dm.de/pflege-und-parfum/koerperpflege/bodylotion-und-hautcreme",
        "https://www.dm.de/ausverkauf",
        "https://www.dm.de/p/d/1493055/head-und-shoulders-shampoo-anti-schuppen-classic-clean",
        "https://www.dm.de/p/d/1493055",
        "https://dm.de/p/d/1493055"
    ],
    "brands": [],
    "proxy": {
        "useApifyProxy": true
    },
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/dm-de-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 = {
    "mode": "search",
    "searchTerm": "shampoo",
    "urls": [
        "https://www.dm.de/search?query=shampoo",
        "https://www.dm.de/search?query=shampoo&currentPage=1",
        "https://www.dm.de/haare",
        "https://www.dm.de/haare?currentPage=1",
        "https://www.dm.de/pflege-und-parfum/koerperpflege/bodylotion-und-hautcreme",
        "https://www.dm.de/ausverkauf",
        "https://www.dm.de/p/d/1493055/head-und-shoulders-shampoo-anti-schuppen-classic-clean",
        "https://www.dm.de/p/d/1493055",
        "https://dm.de/p/d/1493055",
    ],
    "brands": [],
    "proxy": { "useApifyProxy": True },
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/dm-de-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 '{
  "mode": "search",
  "searchTerm": "shampoo",
  "urls": [
    "https://www.dm.de/search?query=shampoo",
    "https://www.dm.de/search?query=shampoo&currentPage=1",
    "https://www.dm.de/haare",
    "https://www.dm.de/haare?currentPage=1",
    "https://www.dm.de/pflege-und-parfum/koerperpflege/bodylotion-und-hautcreme",
    "https://www.dm.de/ausverkauf",
    "https://www.dm.de/p/d/1493055/head-und-shoulders-shampoo-anti-schuppen-classic-clean",
    "https://www.dm.de/p/d/1493055",
    "https://dm.de/p/d/1493055"
  ],
  "brands": [],
  "proxy": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false
}' |
apify call abotapi/dm-de-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/dm-de-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/tdCzjv35ZOMyD28fc/builds/PAckRsOeLKJFVeNPa/openapi.json
