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
16 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 Danish OTC Medicine Price Scraper 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.
📚 What data can you extract from Danish pharmacies?
Every record carries 39 fields, grouped like this:
| Group | What you get |
|---|---|
| Core listing | recordType, retailer, retailerProductId, name, brand, vnr, ean, onSale, packText, size, packQuantity, packUnit …and 10 more |
| Pay & budget | price, listPrice, inStock, pricePerUnit, isFixedPrice, previousPrice, priceChange, priceChangePct, previousPricePerUnit, previouslyInStock, cheapestPrice, minPrice …and 1 more |
| Dates & change tracking | changeTypes, previousScrapedAt |
Every field is nullable — missing values come back as null. Compact mode returns
the core fields only for lean AI/MCP payloads, and excludeEmptyFields drops empties
entirely.
✨ Key features
- 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.
💡 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.
🚀 How to scrape Danish pharmacies
- 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
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.
💡 What can you do with Danish pharmacy price data?
Track competitor pricing across the liberalized OTC market
Watch what Apopro, Webapoteket and med24 charge for the same varenummer, day by day. Because
every item carries pricePerUnit, you compare cost per dose rather than cost per pack.
Price-monitoring alerts for a product portfolio
Schedule a run over your product list, turn on monitoring, and route the changes to Slack or a webhook. You hear about a competitor's price move the day it happens, not at the next review.
Retail and category analysis
Export the full comparison output to CSV or Excel and analyse spread, pack-size strategy and which categories carry the widest cross-pharmacy gap.
Brand and manufacturer visibility
See where your products are listed, at what price, in what pack sizes, and which pharmacy is undercutting the rest — including which items are fixed-price by law and therefore not competitive on price at all.
Comparison sites and price-feed pipelines
Clean, typed JSON straight into your own comparison page, sheet or warehouse, refreshed on a schedule with no manual collection.
💰 How much does it cost to scrape Danish pharmacies?
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.
🔌 Integrations & export
Export to JSON, CSV, Excel or an HTML table, or pull from the REST API and the JavaScript / Python clients. Runs on a schedule, connects to Google Sheets, Slack, Make, Zapier and n8n, and works as an MCP tool for AI agents — compact mode keeps token usage small.
🔗 Using the API
Run this Actor from your own code. Example with the Apify Python client:
from apify_client import ApifyClientclient = ApifyClient("<YOUR_API_TOKEN>")run = client.actor("corvuslab/dk-otc-price-scraper").call(run_input={})for item in client.dataset(run["defaultDatasetId"]).iterate_items():print(item)
It also works with the JavaScript/TypeScript client, the Apify CLI and the REST API.
❓ FAQ
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.
Keywords: danish pharmacy price scraper · OTC medicine prices denmark · håndkøbsmedicin priser · apopro scraper · webapoteket scraper · med24 scraper · parapharmacy price comparison · varenummer price match · pharmacy price monitoring · medicine price api · denmark health product prices · price comparison scraper · market research · export to CSV/Excel/JSON · no-code price scraper

