Shop Apotheke Product & Price Scraper (Germany)
Pricing
from $1.40 / 1,000 product scrapeds
Shop Apotheke Product & Price Scraper (Germany)
Shop Apotheke scraper: search terms to one row per product with EUR price, UVP/AVP list price, discount percent, PZN, brand, pack size, base price, rating, stock, prescription flag and seller. For OTC pharmacy and drugstore price watching in Germany.
Pricing
from $1.40 / 1,000 product scrapeds
Rating
0.0
(0)
Developer
Superslow Sloth
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
4 days ago
Last modified
Categories
Share
Search Shop Apotheke, the German online pharmacy, by keyword and get one flat row per product: the current price in EUR, the UVP/AVP list price and the discount percent, the PZN, brand, pack size, price per unit, rating, stock status, whether it is a prescription medicine, and who sells it. Run it on a schedule and diff the dataset to get an OTC pharmacy price watch for Germany: which ibuprofen, vitamin D or nasal-spray packs went up or down, and how far each sits below its list price.
No account, no API key, no login, no browser. One request returns up to 100 products.
Input
| Field | Notes |
|---|---|
searchTerms | One or more search terms, one per line (ibuprofen 400, vitamin d, Voltaren). Each term is read on its own, down to the first 5,000 matches the shop serves for it. A product found by two terms is returned once, under the first. |
startUrls | Shop Apotheke search pages, such as https://www.shop-apotheke.com/search.htm?q=ibuprofen; each is read as its search term. Category and brand pages (/erkaeltung/, /marken/voltaren/) are refused with the reason, see the limits below. Give terms, URLs or both. |
maxItems | Products to return per term (default 100, up to 5,000). |
onlyOnSale | Return only products the shop marks as reduced (on_sale true). The others are read but not returned and not charged. |
sort | relevance (default), topseller, price-asc, price-desc, newest: the five choices of the site's own sort menu. |
proxyConfiguration | Apify datacenter proxy by default. |
Example input:
{"searchTerms": ["ibuprofen 400", "vitamin d"],"maxItems": 200,"onlyOnSale": false,"sort": "relevance"}
Output
Prices are numbers in euros (the shop stores whole cents; 399 becomes 3.99).
currency is the currency the offer states, EUR on every product measured.
| Field | Notes |
|---|---|
query, position | The search term that first found the product and its place in the shop's list for that term and sort (1 is first). |
product_id, pzn, ean | The shop's product id, the PZN (Pharmazentralnummer) and the EAN barcode. pzn is null for marketplace partners' products, which have no PZN (their product_id starts with upm). |
name, brand, manufacturer | As the shop shows them. brand falls back to the shop's internal brand when the public one is missing. |
url, image_url | The product page and its photo. |
price, list_price | price is the price now. list_price is the UVP/AVP the shop strikes through. They are equal when the shop shows no reduction. Null when the shop sends no price. |
on_sale, discount_percent | on_sale is true when the shop reports a discount. discount_percent is the shop's own whole percent (it rounds). Null when the product is not reduced, never 0. |
pack_size, base_price, base_price_unit | The pack (20 St), and the shop's price per unit (0.17 per 1 St). Null when the shop sends none. |
rating, rating_count | Average of customer ratings and how many. rating is null when nobody has rated the product; the shop sends 0.0 for that and it is not a score. |
in_stock, stock_status | The shop's flag and its status word as sent (available, out_of_stock, out_of_sale, not_in_catalog). An unavailable product can still carry a price. |
prescription_only | True for a prescription medicine (Rx), which the shop lists but can only dispense against a prescription. |
seller, marketplace_seller | Who holds the best offer: Shop Apotheke itself or a marketplace partner (marketplace_seller true). |
variant_count | How many variants (sizes, flavours) the product has. Counted, not expanded: each variant is its own search hit when it is a separate product. |
Example row (real values, 2026-10-16):
{"query": "Hansaplast Erste-Hilfe-Set","position": 3,"product_id": "11663488","pzn": "11663488","name": "Hansaplast Erste Hilfe Pflaster Mix","brand": "Hansaplast","manufacturer": "Beiersdorf AG","url": "https://www.shop-apotheke.com/arzneimittel/11663488/hansaplast-erste-hilfe-pflaster-mix-strips.htm","image_url": "https://cdn.shop-apotheke.com/images/D11/663/488/D11663488-p1.jpg","ean": "9005800330624","currency": "EUR","price": 3.49,"list_price": 3.75,"on_sale": true,"discount_percent": 7,"pack_size": "20 St","base_price": 0.17,"base_price_unit": "1 St","rating": 4.8,"rating_count": 23,"in_stock": true,"stock_status": "available","prescription_only": false,"seller": "Shop Apotheke","marketplace_seller": false,"variant_count": 1}
Honest nulls
A field the shop did not send is null, never 0 or "". Some hits carry no
price at all; their row is still returned with null price, and so are products
that are out of stock but still priced. Marketplace products have no PZN.
Pricing
Pay per event:
- $0.002 per run (
actor-start), charged only after your input is validated, so a run that fails on bad input costs nothing. - $0.0014 per product row written (
product-scraped), i.e. $1.40 per 1,000 products. A row that is not written is not charged: rows filtered byonlyOnSale, and repeats of a product another term already returned, are free.
How it fetches
Shop Apotheke's search box queries an Algolia search index, and its public
search-only key is embedded in every page the shop serves. The actor asks that
same index for the same results, one request per page of up to 100 products,
the way the shop's own search does. The HTML search page cannot be used for
paging: it shows 30 products and ignores page, p and i (each was tried
on 2026-10-16). A refusal (403, 429, 5xx), a 200 that is not the index's answer,
or a dropped connection is treated as the address being blocked, and the actor
rotates to a fresh one and retries.
Things worth knowing, all measured on 2026-10-16:
- Only the first 5,000 matches of a term can be read. The index answers a
page past that with an empty list. A term with more matches than that is
logged with its count; narrow the term (
ibuprofen 400 20 st) to see the rest. The match count is approximate: the engine does not count exhaustively. - A term is matched loosely. Typo tolerance is on, so a nonsense word can still match a few products, and a very common word matches thousands.
- Category and brand pages are not supported. Their product list is loaded
by the browser after the page, with filters the page never prints, so only
search URLs are accepted in
startUrls. Use the brand or category name as a search term instead. - Sort orders are separate indexes, not re-orderings. The same term can
match a different set of products under
price-ascthan underrelevance(Hansaplast Erste-Hilfe-Set: 52 matches by relevance, 2 by price). The price orders are not strict for every term: products without a price sit among the priced ones. This is what the shop's own sort menu shows. onlyOnSaleis applied after reading. Asking the index to filter on discount widens the match to unrelated products (metamizolhas 62 matches, none on sale, and the filtered query returned 12 others), so the actor reads the term and keeps the reduced products.maxItemscounts the reduced products returned, so a term with few reductions is read to the end.- The order can differ slightly from a browser. The website personalises its list; 29 of the first 30 products matched in the comparison.
- The key is public but can change. If the shop rotates it, requests are refused with HTTP 403 and the run fails with that message.
Use it from the API
curl -X POST "https://api.apify.com/v2/acts/<your-username>~shop-apotheke-products/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \-H "Content-Type: application/json" \-d '{"searchTerms": ["ibuprofen 400"], "maxItems": 50}'