Danish OTC Medicine Price Scraper
Pricing
from $1.50 / 1,000 results
Danish OTC Medicine Price Scraper
Compare over-the-counter (håndkøbsmedicin) and parapharmacy prices across Danish online pharmacies — Apopro, Webapoteket and med24. Matches the same product by varenummer to show the cheapest shop, with optional price-change monitoring.
Pricing
from $1.50 / 1,000 results
Rating
0.0
(0)
Developer
Corvuslab
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
4 days ago
Last modified
Categories
Share
Compare over-the-counter (OTC) medicine and parapharmacy prices across Danish online pharmacies — Apopro, Webapoteket and med24 — in a single run. This scraper collects live håndkøbsmedicin and health-product prices, matches the same product across shops by Danish varenummer, and tells you instantly which pharmacy is cheapest and by how much. Optional price monitoring turns it into a scheduled price watch that reports every change.
It runs on plain HTTP (no headless browser), so it's fast and low-cost. On the Apify platform you also get scheduling, a REST API, and one-click integrations (Slack, Google Sheets, Make, Zapier, webhooks) for free.
💊 What does this Actor do?
- Scrapes current prices for OTC medicine (håndkøbsmedicin) and parapharmacy (supplements, skincare) from Danish online pharmacies.
- Parses the messy product text into clean fields — brand, pack quantity + unit, strength, active substance, stock and a computed price per unit.
- Compares the identical product across pharmacies by varenummer/EAN and reports the cheapest shop, min/max and the price spread.
- Monitors changes over time — schedule it and each run reports price moves, stock changes, and products added or removed.
🎯 Why use it?
- Exact cross-pharmacy comparison. Apopro and Webapoteket both publish the Danish varenummer, so products are matched precisely — no fuzzy name guessing.
- Fair per-unit pricing. Shops stock different pack sizes (a 20-pack vs a 10-pack), so the Actor computes
pricePerUnit(price per tablet/ml/g) — the honest way to compare cost per dose. - Price monitoring & alerts. Turn on monitoring, schedule it, and pipe the changes to Slack, a webhook or a spreadsheet — a hands-off price watch.
- Knows what's actually comparable. Denmark sets one fixed national price for apoteksforbeholdt (pharmacy-only) medicine, so those are flagged (
isFixedPrice) and left out of "cheapest" logic by default — the price is the same everywhere by law. - Ready for research & pipelines. Clean JSON/CSV/Excel output for pricing analysts, retailers, brands and comparison sites tracking the liberalized OTC market.
🏥 Which Danish pharmacies are covered?
| Pharmacy | Status | Product identifier |
|---|---|---|
| Apopro | ✅ Live | varenummer (Vnr) |
| Webapoteket | ✅ Live | varenummer (Vnr) + EAN |
| med24 | ✅ Live | EAN (GTIN) |
| Matas | 🔜 Planned | EAN |
Apopro ↔ Webapoteket are matched exactly on varenummer. med24 publishes an EAN instead, so its items compare within the EAN group (more EAN pharmacies coming). Adapters are modular — new pharmacies slot in without changing the output format.
🚀 How to use it
- Click Try for free / Start.
- Choose your retailers (default: Apopro + Webapoteket + med24) and segments (default: OTC medicine).
- (Optional) Add search terms (product names or varenumre) to look up specific products instead of browsing whole segments.
- (Optional) Set Max products per retailer for a quick test run.
- Run it, then download the results from the Dataset tab as JSON, CSV, Excel or HTML — or pull them via the API.
⚙️ Input
| Field | Type | Default | Description |
|---|---|---|---|
retailers | array | ["apopro","webapoteket","med24"] | Which pharmacies to scrape. matas is reserved for a future version and skipped if selected. |
segments | array | ["otc_medicine"] | otc_medicine (håndkøbsmedicin) and/or parapharmacy (supplements, skincare). Ignored when searchTerms is set. |
searchTerms | array | [] | Product names or varenumre to search at each retailer. Overrides segment browsing. |
emitComparisons | boolean | true | Emit a cross-retailer comparison per product carried by 2+ selected retailers (matched by varenummer/EAN). |
includeFixedPrice | boolean | false | Include apoteksforbeholdt fixed-price items in comparisons (off by default — same price everywhere). |
emitPriceRecords | boolean | true | Output the full price record per product. Turn off (with comparisons or monitoring on) for leaner output. |
monitorChanges | boolean | false | Diff this run against the previous run of the same scope and emit change records. First run saves a baseline. Pair with an Apify Schedule. |
stateKey | string | (auto) | Names the monitoring baseline. Use different keys for independent price-watches, or the same key to share one baseline. Empty = auto-derive from your inputs. |
resolveIdentifiers | boolean | false | Visit med24/Webapoteket product pages to resolve each item's EAN. Roughly doubles requests; Apopro's Vnr is always read from the listing. |
maxProductsPerRetailer | integer | 0 | Cap per retailer (0 = no limit). |
maxConcurrency | integer | 8 | Max parallel HTTP requests. |
No proxy configuration is needed — these sites are lightweight and crawled directly over plain HTTP.
📤 Output modes
The dataset can hold up to three record types, shown by the recordType column. What gets emitted is controlled by three toggles:
emitPriceRecords(default on) →retailer_pricerows: one per product per shop — the raw price, size, stock and per-unit price. The full price list.emitComparisons(default on) →comparisonrows: one per product carried by 2+ selected shops — cheapest shop, min/max and the spread. Matched exactly on varenummer/EAN, never by name.includeFixedPrice(default off) → apoteksforbeholdt (pharmacy-only) medicines have one fixed national price, identical everywhere. Off = excluded from comparisons (their spread is always 0); on = included anyway.
Tip: dataset views choose columns but can't hide rows, so a mixed run shows blank cells for the other type. For a clean table, pick one:
emitPriceRecords: false→ comparisons only;emitComparisons: false→ price list only.
How to read a comparison row:
name · vnr · retailerCount · cheapestRetailer · cheapestPrice · minPrice · maxPrice · spread · spreadPct
"This exact product (by varenummer) is sold by
retailerCountshops; cheapest ischeapestRetaileratcheapestPricekr; the gap between shops isspreadkr (spreadPct%)." A spread of 0 means the shops price-match that product — no saving to be had.
🧾 Output examples & data fields
Each product produces one retailer_price record:
{"recordType": "retailer_price","retailer": "apopro","url": "https://apopro.dk/produkt/panodil-...-409193","name": "Panodil","brand": "Panodil","vnr": "409193","ean": null,"price": 32.8,"inStock": true,"size": "20 tabletter · 500 mg","packQuantity": 20,"packUnit": "tabletter","pricePerUnit": 1.64,"strength": "500 mg","activeSubstance": "Paracetamol","isFixedPrice": true,"scrapedAt": "2026-07-07T12:00:00.000Z"}
You can download the dataset as JSON, CSV, Excel or HTML. It ships with four ready-made views: Prices, OTC medicine (Vnr), Comparisons and Changes.
Data fields (retailer_price)
| Field | Description |
|---|---|
retailer | Retailer key (apopro, webapoteket, med24). |
name, brand | Product name and brand. |
vnr | Danish varenummer (Apopro, Webapoteket). Primary cross-retailer match key. |
ean | EAN/GTIN (med24, and Webapoteket/med24 medicine when resolveIdentifiers is on). Fallback match key. |
price, listPrice | Current price and recommended/previous price (DKK), or null if not shown. |
onSale, inStock | Sale and stock flags. |
size | Clean combined size, e.g. "20 tabletter · 500 mg". |
packQuantity, packUnit | How much is in the pack — e.g. 20 + "tabletter", or 10 + "ml". |
pricePerUnit | Price per single unit (per tablet/ml/g) in DKK — compare shops fairly across pack sizes. |
strength | Dose per unit, e.g. "500 mg", "500+50 mg", "1 mg/ml". |
activeSubstance | Active substance, where the retailer exposes it. |
isFixedPrice | true for apoteksforbeholdt items with a fixed national price. |
category, url, scrapedAt | Category label, product URL, scrape timestamp (ISO). |
⚖️ Comparison output
Each product carried by 2+ selected pharmacies produces a comparison record. The Comparisons view sorts them by biggest spread.
{"recordType": "comparison","matchType": "vnr","matchKey": "184963","name": "Ipren tabletter","size": "20 tabletter · 200 mg","offers": [{ "retailer": "apopro", "price": 28.95, "inStock": true },{ "retailer": "webapoteket", "price": 25.0, "inStock": true }],"retailerCount": 2,"cheapestRetailer": "webapoteket","cheapestPrice": 25.0,"minPrice": 25.0,"maxPrice": 28.95,"spread": 3.95,"spreadPct": 15.8}
Matching is exact, on the shared identifier (varenummer for Apopro ↔ Webapoteket, EAN for the EAN group) — never on product name. Fixed-price items are excluded unless includeFixedPrice is on.
🔔 Price monitoring & change alerts
With monitorChanges on, runs after the first emit change records — one per product that changed since the previous run of the same scope:
{"recordType": "change","changeTypes": ["price_changed"],"retailer": "apopro","name": "Panodil","previousPrice": 29.8,"price": 32.8,"priceChange": 3.0,"priceChangePct": 10.07,"previousScrapedAt": "2026-07-06T06:00:00.000Z","scrapedAt": "2026-07-07T06:00:00.000Z"}
Change types: added, removed, price_changed, back_in_stock, out_of_stock (a record can carry more than one). The baseline persists between runs, so nothing recurring to set up on your side.
Set up a price-drop alert (no code):
- Set
monitorChanges: true— andemitPriceRecords: falsefor alert-only output. - Attach an Apify Schedule (e.g. daily or hourly).
- In the Actor's Integrations tab, connect Slack, Discord, email, a webhook or Google Sheets to the run's dataset.
- Each run pushes only what changed, so you get alerts without duplicates. Give separate watches their own
stateKeyto keep them independent.
💸 How much does it cost?
This Actor is pay-per-event: a small fee at run start plus a per-record fee. It uses lightweight HTTP requests, so the underlying platform cost is a fraction of a cent per run.
Real example from testing: a full OTC (håndkøbsmedicin) run across Apopro + Webapoteket produced about 1,230 price records and 289 exact-varenummer comparisons — of which 144 showed a real price difference between the two pharmacies. Use maxProductsPerRetailer to cap runs while testing, and searchTerms to track just a basket of products cheaply.
See the Actor's Pricing tab for exact current prices.
💡 Tips & advanced options
- For price monitoring, turn on
monitorChanges, add a Schedule, and setemitPriceRecords: falsefor lean, alert-style output. - For a specific basket, use
searchTerms(product names or varenumre) instead of scraping whole segments. - For the cleanest table, run one output type at a time (
emitComparisons: falsefor a plain price list, oremitPriceRecords: falsefor comparisons only). - Leave
resolveIdentifiersoff unless you specifically need EANs — it roughly doubles requests.
❓ FAQ, disclaimer & support
Is scraping these prices legal? The Actor reads only public, publicly listed prices from product pages (no login, no personal data) and stays polite with rate limiting. Monitoring publicly listed prices for comparison is common practice, but you are responsible for complying with each site's Terms of Service.
Is this medical advice? No. This is pricing and market data only — not medical advice. This Actor is not affiliated with the retailers or the Danish Medicines Agency (Lægemiddelstyrelsen). No personal or GDPR-relevant data is processed.
How is the same product matched across shops? By the Danish varenummer (Apopro ↔ Webapoteket) or EAN (EAN pharmacies) — an exact identifier match, never a name guess. med24 currently publishes only an EAN, so med24 ↔ Apopro/Webapoteket matching will broaden as more EAN pharmacies are added.
Support / feedback. Found a broken selector or want another pharmacy added? Open an issue on the Actor's Issues tab. Custom pharmacy additions are available on request.