# Shop Apotheke Product & Price Scraper (Germany) (`superslowsloth/shop-apotheke-products`) Actor

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.

- **URL**: https://apify.com/superslowsloth/shop-apotheke-products.md
- **Developed by:** [Superslow Sloth](https://apify.com/superslowsloth) (community)
- **Categories:** E-commerce, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.40 / 1,000 product scrapeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## Shop Apotheke Product & Price Scraper (Germany)

Search [Shop Apotheke](https://www.shop-apotheke.com), 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:

```json
{
  "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):

```json
{
  "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 by
  `onlyOnSale`, 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-asc` than under `relevance`
  (`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.
- **`onlyOnSale` is applied after reading.** Asking the index to filter on
  discount widens the match to unrelated products (`metamizol` has 62 matches,
  none on sale, and the filtered query returned 12 others), so the actor reads
  the term and keeps the reduced products. `maxItems` counts 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

```bash
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}'
```

# Actor input Schema

## `searchTerms` (type: `array`):

What you would type into the Shop Apotheke search box, one term per line (for example ibuprofen 400, vitamin d or Voltaren). Each term is read separately, down to the 5,000 best matches the shop serves for it. A product that two terms both find is returned once, under the first.

## `startUrls` (type: `array`):

Shop Apotheke search pages, for example 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: the shop loads their product list after the page, so type the product or brand name as a search term instead. Leave empty to use search terms only.

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

Return at most this many products for each search term. The shop serves at most the first 5,000 matches of a term, so that is the largest value accepted. With 'Only products on sale' the limit counts the reduced products returned, not the ones read.

## `onlyOnSale` (type: `boolean`):

Return only products the shop marks as reduced (discount percent above 0, the on_sale field). The other products of the term are read but not returned and not charged. The shop's own sale filter is not used: it was measured to widen the match to unrelated products.

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

The order Shop Apotheke lists the products in, the same five choices as its own sort menu. Relevance is its default. Each choice reads a separate index of the shop, so the same term can match a different set of products under another sort, and the price orders are not strict (products without a price sit among the priced ones).

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

Use Apify's automatic datacenter proxy (the default). The shop's search index answered every request from the development machine without a proxy; a refused request is retried from a fresh address when a proxy is on.

## Actor input object example

```json
{
  "searchTerms": [
    "ibuprofen"
  ],
  "startUrls": [],
  "maxItems": 100,
  "onlyOnSale": false,
  "sort": "relevance",
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `items` (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 = {
    "searchTerms": [
        "ibuprofen"
    ],
    "startUrls": [],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("superslowsloth/shop-apotheke-products").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 = {
    "searchTerms": ["ibuprofen"],
    "startUrls": [],
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("superslowsloth/shop-apotheke-products").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 '{
  "searchTerms": [
    "ibuprofen"
  ],
  "startUrls": [],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call superslowsloth/shop-apotheke-products --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,superslowsloth/shop-apotheke-products"
        }
    }
}
```

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/sGeAg85PAXnHbuSoa/builds/N3vngnD3WKe2X0bPV/openapi.json
