dm API & Scraper
The dm API turns dm-drogerie markt — Europe's largest drugstore chain — into clean JSON in six actions: search, product/detail, reviews, availability, stores and suggest.
🤖 Using an AI assistant? Copy this link into ChatGPT / Claude / Cursor — it reads every endpoint and parameter instantly and tells you if this API fits your use case.
One codebase serves 13 online storefronts on the same platform (DE, AT, CZ, SK, HU, SI, HR, RO, BG, RS, BA, IT, PL); a validated country parameter picks the store, and currency and language follow it (EUR, CZK, HUF, RON, RSD, BAM, PLN). search takes a keyword and/or a category, offers dm's own sorts (relevance, price, newest, top-rated) and clean filters (brand, product feature, popular filter, price range), and returns rows carrying the dm article number (DAN), the GTIN, the brand, the shopper price, the excl-VAT net price, the struck was-price with its discount, the EU 30-day lowest price when the store shows one, the price per unit, the rating and count, the dm own-brand / online-only / new / sale flags, the pharmacy flag, the variant axis and the images. product/detail adds the ingredient list, the full description with bullet points, the usage text, the category breadcrumb, the dm-Dauerpreis 'not raised since' date, the Payback line, the seals and pills, and every size/shade variant. reviews returns the average rating, the 1-5 star distribution and the 'would recommend' share plus the full review text; availability returns whether each product is deliverable online and, given a store id, the in-store stock; stores is the dm store finder (by latitude/longitude, bounding box or store id); suggest is the autocomplete. A no-result search comes back empty (dm's own signal, never padded), and a partly-matching multi-word query keeps the matched rows and reports the dropped words. In a live check on 2026-09-15, every search row across all 13 countries carried a price, and across 14 products in 5 categories the dan, GTIN, price, net price and currency matched the raw catalogue exactly, with the search-row price equal to the detail price on 14 of 14. No dm account, no browser, one ReefAPI key and the standard { ok, data, meta, error } envelope.
The price you see is the gross shopper price — and dm publishes several other numbers beside it
A dm product commonly carries more than one number: the price a shopper pays, the excl-VAT net price (lower), the struck was-price on a discount, the EU-mandated lowest price of the last 30 days, and the per-unit base price. The API keeps every one in its own field so the net price is never mistaken for the shopper price, a discount is never invented, and the per-unit price is never confused with the pack price.
| Product (country) | price (gross) | net_price (excl VAT) | was_price | price per unit |
|---|---|---|---|---|
| Balea med Shampoo Ultra Sensitive 250 ml (DE) | 1.75 | 1.47 | null | 7.00 €/1 l |
| Seifendose (DE, on sale) | 1.95 | 1.64 | 2.55 | 1.95 €/St |
| Balea serum 30 ml (PL, Omnibus) | 69.95 zł | null | 84.95 zł | 233.17 zł/100 ml |
| Balea med shampoo 300 ml (RS) | 119.00 RSD | null | null | 39.67 RSD/100 ml |
net_price is the price excluding VAT (lower than the shopper price); it is returned only where dm publishes it and is never the shopper price. lowest_price_30d is the EU-Omnibus figure, parsed from dm's own localized note and separate from was_price. base_price is the price per 100 ml / per litre / per kg, separate from the pack price. dm's Payback is a points programme, not a member price, so there is no logged-in-only price.
Real request and response JSON
Captured from the indexed primary action, search, on .
{
"method": "POST",
"url": "https://api.reefapi.com/dm/v1/search",
"headers": {
"x-api-key": "$REEF_KEY",
"content-type": "application/json"
},
"body": {
"country": "de",
"query": "shampoo"
}
}{
"ok": true,
"meta": {
"api": "dm",
"endpoint": "search",
"mode": "live",
"latency_ms": 385.5,
"record_count": 30,
"bytes": 99891,
"cache_hit": false,
"pagination": {
"page": 1,
"page_size": 30,
"total": 579,
"has_more": true
}
},
"data": {
"results": [
{
"dan": 1569035,
"gtin": "4066447966008",
"product_name": "Shampoo Ultra Sensitive, 250 ml",
"brand": "Balea med",
"url": "https://www.dm.de/p/d/1569035/balea-med-shampoo-ultra-sensitive",
"price": 1.75,
"currency": "EUR",
"was_price": null,
"discount_percent": null,
"net_price": 1.47,
"lowest_price_30d": null,
"base_price": {
"amount": 7,
"unit": "€",
"per_amount": 1,
"per_unit": "l",
"text": "0,25 l (7,00 € je 1 l)"
},
"price_notes": [
"0,25 l (7,00 € je 1 l)"
],
"rating": 3.95,
"rating_count": 456,
"rating_scale": 5,
"is_dm_brand": true,
"is_pharmacy": false,
"online_only": false,
"is_new": false,
"on_sale": false,
"flags": [
"dm_brand"
],
"categories": [
"Shampoo"
],
"variant_count": null,
"variants": null,
"images": [
"https://products.dm-static.com/images/f_auto,q_auto,c_fit,h_320,w_320/v1772150521/assets/pas/images/1fcbf079-eaca-4c6f-bf93-ba222052d98a/balea-med-shampoo-ultra-sensitive",
"https://products.dm-static.com/images/f_auto,q_auto,c_fit,h_320,w_320/v1772150525/assets/pas/images/cb9e56bf-1edd-41e1-959d-c080a9962dbf/balea-med-shampoo-ultra-sensitive",
"https://products.dm-static.com/images/f_auto,q_auto,c_fit,h_320,w_320/v1772150516/assets/pas/images/836e3e0c-5434-40a1-bf15-1d23dffab570/balea-med-shampoo-ultra-sensitive"
],
"image": "https://products.dm-static.com/images/f_auto,q_auto,c_fit,h_320,w_320/v1772150521/assets/pas/images/1fcbf079-eaca-4c6f-bf93-ba222052d98a/balea-med-shampoo-ultra-sensitive"
},
{
"dan": 1376376,
"gtin": "4066447965414",
"product_name": "Duschgel & Shampoo 5% Urea 2in1, 300 ml",
"brand": "Balea med",
"url": "https://www.dm.de/p/d/1376376/balea-med-duschgel-und-shampoo-5-prozent-urea-2in1",
"price": 1.75,
"currency": "EUR",
"was_price": null,
"discount_percent": null,
"net_price": 1.47,
"lowest_price_30d": null,
"base_price": {
"amount": 5.83,
"unit": "€",
"per_amount": 1,
"per_unit": "l",
"text": "0,3 l (5,83 € je 1 l)"
},
"price_notes": [
"0,3 l (5,83 € je 1 l)"
],
"rating": 4.56,
"rating_count": 216,
"rating_scale": 5,
"is_dm_brand": true,
"is_pharmacy": false,
"online_only": false,
"is_new": false,
"on_sale": false,
"flags": [
"dm_brand"
],
"categories": [
"Shampoo",
"Duschgel"
],
"variant_count": null,
"variants": null,
"images": [
"https://products.dm-static.com/images/f_auto,q_auto,c_fit,h_320,w_320/v1785447022/assets/pas/images/8f7657d4-053c-48d4-a774-59f967d3164c/balea-med-duschgel-und-shampoo-5-prozent-urea-2in1",
"https://products.dm-static.com/images/f_auto,q_auto,c_fit,h_320,w_320/v1785447026/assets/pas/images/2096efc1-a19a-4ad0-b83a-652d1a3d60a3/balea-med-duschgel-und-shampoo-5-prozent-urea-2in1",
"https://products.dm-static.com/images/f_auto,q_auto,c_fit,h_320,w_320/v1785447030/assets/pas/images/7e9fa68e-019e-4a6a-881d-f1e69e2f0844/balea-med-duschgel-und-shampoo-5-prozent-urea-2in1"
],
"image": "https://products.dm-static.com/images/f_auto,q_auto,c_fit,h_320,w_320/v1785447022/assets/pas/images/8f7657d4-053c-48d4-a774-59f967d3164c/balea-med-duschgel-und-shampoo-5-prozent-urea-2in1"
},
{
"dan": 3130636,
"gtin": "8006530290012",
"product_name": "Shampoo Fiji, 350 ml",
"brand": "Herbal Essences",
"url": "https://www.dm.de/p/d/3130636/herbal-essences-shampoo-fiji",
"price": 3.95,
"currency": "EUR",
"was_price": null,
"discount_percent": null,
"net_price": 3.32,
"lowest_price_30d": null,
"base_price": {
"amount": 11.29,
"unit": "€",
"per_amount": 1,
"per_unit": "l",
"text": "0,35 l (11,29 € je 1 l)"
},
"price_notes": [
"0,35 l (11,29 € je 1 l)"
],
"rating": 4.72,
"rating_count": 349,
"rating_scale": 5,
"is_dm_brand": false,
"is_pharmacy": false,
"online_only": false,
"is_new": false,
"on_sale": false,
"flags": null,
"categories": [
"Shampoo"
],
"variant_count": null,
"variants": null,
"images": [
"https://products.dm-static.com/images/f_auto,q_auto,c_fit,h_320,w_320/v1773867112/assets/pas/images/1cbddc2d-00a6-4154-9727-f626992bc65c/herbal-essences-shampoo-fiji",
"https://products.dm-static.com/images/f_auto,q_auto,c_fit,h_320,w_320/v1773867120/assets/pas/images/08d7be06-9558-40ca-bdde-3115a2940401/herbal-essences-shampoo-fiji",
"https://products.dm-static.com/images/f_auto,q_auto,c_fit,h_320,w_320/v1779234915/assets/pas/images/feb8c819-c699-4210-882e-90db9af278ed/herbal-essences-shampoo-fiji"
],
"image": "https://products.dm-static.com/images/f_auto,q_auto,c_fit,h_320,w_320/v1773867112/assets/pas/images/1cbddc2d-00a6-4154-9727-f626992bc65c/herbal-essences-shampoo-fiji"
}
],
"count": 30,
"total_results": 579,
"total_pages": 20,
"page": 1,
"page_size": 30,
"sort": "relevance",
"query": "shampoo",
"category": null,
"country": "de",
"currency": "EUR",
"predicted_category": "Haare > Haarpflege > Shampoo",
"modified_query": null,
"unmatched_terms": null
}
}What the dm API does
| Action | Description | Concrete use case | Key params |
|---|---|---|---|
| search | Search a dm storefront by keyword and/or category, with the site's own sorts and filters (brand, feature, popular filter, price range). Each row: dm article number (DAN), GTIN, brand, name, the shopper price + currency, the struck was-price + discount, the excl-VAT net price, the price per unit, the EU 30-day lowest price when shown, rating + count, dm own-brand/online-only/new/sale flags, pharmacy flag, variant axis and images. Ingredients and the full description are on product/detail. A no-result search returns an empty list (dm's own signal — no padding); a partly-matching multi-word query keeps the matched rows and reports the unmatched words as modified_query. | Pricing teams call search to search a dm storefront by keyword and/or category, with the site's own sorts and filters (bra…. | country, query, category, sort, page, ... |
| product/detail | The full dm product by article number, GTIN or URL: name, brand, GTIN, the shopper price with was-price/discount, the excl-VAT net price, the price per unit, the EU 30-day lowest price, the dm-Dauerpreis 'not raised since' date, the Payback line, availability of ratings, the category breadcrumb + codes, the description with bullet points, the ingredient list, the usage text, pills/seals, images, and every size/shade variant. | Marketplace operators call product/detail to get the full dm product by article number, GTIN or URL. | country, product_id, gtin, url, max_rotations |
| reviews | Ratings and reviews for a dm product: the average rating and count, the 1–5 star distribution and the 'would recommend' share (per country), plus the full review text — title, body, rating, nickname, date, recommend flag, photos — from the German (dm-de) review deployment. | Catalog enrichment teams call reviews to get ratings and reviews for a dm product. | product_id, country, limit, max_rotations |
| availability | Online deliverability for dm products by article number, and per-store stock when a store id is given. Returns whether each product is purchasable online and, with store_id, the in-store availability status for that dm market. | Retail analysts call availability to get online deliverability for dm products by article number, and per-store stock when a store id…. | product_ids, country, store_id, max_rotations |
| stores | The dm store finder: list dm markets near a latitude/longitude (or in a bounding box) for a country, or fetch one store by id. Each store: id, number, address, coordinates, phone, opening hours, services, express-pickup flag and popular-times. | Pricing teams call stores to get the dm store finder. | country, lat, lon, min_results, bbox, ... |
| suggest | dm search autocomplete for a partial query: the ranked keyword suggestions. | Marketplace operators call suggest to get dm search autocomplete for a partial query. | query, country, max_rotations |
Call search from your stack
curl -X POST https://api.reefapi.com/dm/v1/search \
-H "x-api-key: $REEF_KEY" \
-H "content-type: application/json" \
-d '{"country":"de","query":"shampoo"}'import requests
r = requests.post(
"https://api.reefapi.com/dm/v1/search",
headers={"x-api-key": REEF_KEY},
json={
"country": "de",
"query": "shampoo"
},
)
print(r.json()["data"])const res = await fetch("https://api.reefapi.com/dm/v1/search", {
method: "POST",
headers: {
"x-api-key": process.env.REEF_KEY,
"content-type": "application/json",
},
body: JSON.stringify({
"country": "de",
"query": "shampoo"
}),
});
const { ok, data, meta, error } = await res.json();Ask your MCP-connected assistant: call reefapi.dm.search with {"country":"de","query":"shampoo"}.Who uses this API and why
- Drugstore price-monitoring teams track a product's shopper price, net price, was-price, the EU 30-day lowest price and the price per unit across up to 13 countries from one API.
- Brands watch where their SKUs are listed, at what price and under which dm-Dauerpreis, and whether dm's own Balea/alverde lines undercut them.
- Catalog teams pull the GTIN, ingredients and every size variant to match products by barcode across countries.
- Retail and logistics tools read online deliverability and per-store stock, and locate the nearest dm markets with the store finder.
Questions developers ask before integrating
Which countries does it cover, and what currency comes back?
Thirteen online storefronts on one platform: Germany, Austria, Czechia, Slovakia, Hungary, Slovenia, Croatia, Romania, Bulgaria, Serbia, Bosnia & Herzegovina, Italy and Poland. Pass country; currency and language follow it (EUR, CZK for Czechia, HUF for Hungary, RON for Romania, RSD for Serbia, BAM for Bosnia, PLN for Poland). dm North Macedonia has stores but no online shop, and dm Switzerland has no online shop, so both return a clear MARKET_UNAVAILABLE.
What is the difference between price and net_price?
price is the gross price a shopper pays. net_price is the same price excluding VAT, which dm shows for business buyers and is always lower — so it must never be used as the shopper price. On a discounted product the was_price is the struck gross price (with discount_percent), not the net price. Every figure is the store's own currency, and dm's formatted strings are never returned as the number.
How do I get the EU 30-day lowest price and the per-unit price?
lowest_price_30d is the EU-Omnibus lowest price of the previous 30 days, which dm prints as a localized note; the API parses it to a number when it is unambiguous and also returns the raw note in price_notes. base_price is the price per 100 ml / per litre / per kg (amount, unit, per_amount, per_unit), separate from the pack price, so a 30 ml and a 300 ml of the same product are never conflated.
Can I tell a dm own brand from a national brand?
Yes. is_dm_brand is true for dm's own labels (Balea, Balea med, alverde, Denkmit, Profissimo, SUNDANCE and the rest), read from dm's own 'Marke von dm' badge. The row also flags online_only, is_new and on_sale, and is_pharmacy for pharmacy items.
Where do the ingredients, description and variants come from?
From product/detail. Search rows carry the price, brand, rating and per-unit price but not the ingredients; the product record adds the ingredient list, the full description with bullet points, the usage text, the category breadcrumb, the dm-Dauerpreis 'not raised since' date, the Payback line, the seals and pills, and every size/shade variant each with its own dm article number and URL.
Does it return reviews, availability and store data?
Yes. reviews returns the average rating, the 1-5 star distribution and the 'would recommend' share for the country, plus the full review text (from dm's German review deployment, noted as such for other countries). availability returns whether each product is deliverable online and, given a store id, the in-store stock. stores is the dm store finder: pass a latitude and longitude (or a bounding box, or a store id) for a country and get each dm market with its address, coordinates, phone, opening hours, services, express-pickup flag and popular-times.
What happens on a no-result search or a partly-matching query?
A search with no matches returns an empty result list — dm answers with its own empty signal, so there is no page of unrelated padding to filter. A multi-word query where some words do not match keeps the rows that did match and reports the dropped words in modified_query and unmatched_terms, so you always know when dm relaxed the search.
What is the dm API?
dm API is a ReefAPI endpoint group for dm-drogerie markt across 13 countries: prices, price-per-unit, net price, dm own-brand flag, ean, ingredients, ratings, availability and store finder as json. It returns live JSON through POST requests under /dm/v1.
Is the dm API free to try?
Yes. ReefAPI starts with 1,000 free credits, no card required. dm calls use the same shared credit balance as every other ReefAPI engine.
Do I need a dm login or account?
No login to dm is needed for the API response. You call ReefAPI with your x-api-key header, and the playground can run live examples before you create a production key.
How fresh is the dm data?
The page example is captured from a live search call, and production requests fetch live data through ReefAPI rather than a static sample.
How many credits does the dm API use?
dm actions currently cost 1 credit per successful call. Failed or blocked calls are free, and all APIs draw from one credit pool.
Can I call dm from an AI assistant or MCP client?
Yes. Connect ReefAPI once through MCP and your assistant can call dm actions with the same key, credit pool and JSON envelope used by normal REST requests.
Is the dm API a dm scraper?
It is the managed alternative to a DIY dm scraper. Instead of building and maintaining your own scraper — proxies, headless browsers, captcha and constant breakage — you call one ReefAPI endpoint and get the same dm-drogerie markt across 13 countries: prices, price-per-unit, net price, dm own-brand flag, ean, ingredients, ratings, availability and store finder as json back as clean JSON.