A product on Europe's largest online pharmacy came back looking like this:
{
"price": 995,
"listPrice": 1450,
"prices": { "default": { "amount": 9.95 } }
}
Both numbers are correct. price is in cents. prices.default.amount is in euros. Same response, same product, two units, no field name warning you.
Pick the wrong one and you do not get an error — you get a €9.95 product priced at €995, a 100× outlier that sails through every sanity check that only looks for nulls. Then your buyer's "RRP breach" alert fires on the entire catalogue.
Worse: the marketplace-offer array indexes in cents again, so a naive "cheapest across offers" comparison silently mixes both scales.
The fix is not clever, it is just explicit — and the comment matters more than the code:
def _pick_price(raw: dict) -> float | None:
"""Return euros.
Unit trap: top-level `price` / `listPrice` are CENTS, nested
`prices.*.amount` are EUROS, and the offers array is cents again.
Never mix them — a 100x error passes every null check.
"""
nested = (raw.get("prices") or {}).get("default") or {}
if isinstance(nested.get("amount"), (int, float)):
return float(nested["amount"]) # already euros
cents = raw.get("price")
return round(cents / 100, 2) if isinstance(cents, (int, float)) else None
General rule worth internalising: whenever an API exposes the same quantity twice, assume the units differ until you have proved otherwise on a product whose real price you can look up by hand.
The field that does not exist
My first version asked the search backend for a url attribute and got back... nothing. Not an error — just no field, silently absent, on every single record.
The attribute is called deeplink. Same story with the recommended retail price: I asked for strikePrice (a name I had assumed) when the real field is listPrice.
A hosted search API given an unknown attribute name usually omits it rather than complaining. So if a field is mysteriously always empty, check the spelling against a raw unfiltered record before you debug your parser:
ATTRS = ["pzn", "name", "brand", "price", "listPrice", "pricePerUnit",
"stockStatus", "deeplink", "averageRating"]
PZN is the join key, and it is the whole product
German pharmacy retail has something most e-commerce verticals lack: a national article number (PZN) that identifies the exact pack — substance, strength, count, manufacturer. It is the join key that makes cross-retailer comparison honest.
That is what turns a scrape into an asset. With PZN you can:
- compare the same pack across shops without fuzzy title matching
- detect an RRP breach — the shop selling below the manufacturer's list price, which is a competitive signal, not a typo
- watch the marketplace seller undercutting the shop itself on its own product page
- track stock flips per pack, which is the leading indicator for a supply problem
One brand is one request covering up to 1,000 articles, so a competitive set is cheap to watch daily. DOPPELHERZ is 335 articles in a single call; ABTEI 148.
Why daily, and why only the diff
OTC pharmacy prices move constantly — promotions, marketplace sellers, stock. A full snapshot every day is mostly noise you already had. What a category manager acts on is:
if prev is None: change = "new"
elif now.price != prev.price: change = "price_up" / "price_down"
elif now.below_rrp != prev.below_rrp: change = "rrp_breach"
elif now.stock != prev.stock: change = "stock_flip"
else: change = None # never emitted, never billed
Two disciplines that keep this honest:
- A first run has no baseline, so everything looks new. Charging a change price for a snapshot is a bug, not a pricing decision. Bill the baseline as a snapshot or not at all.
- Persist the watermark before charging. Bill first and crash before saving, and the next run re-detects and re-bills the same changes.
Running it
Packaged as DE/AT Pharmacy Price & Stock Monitor: PZN-keyed OTC shelf data across DE and AT — price, list-price breach, discount, availability, and the marketplace seller undercutting the shop. HTTP-only, no browser, no login. Delta mode returns only price moves, RRP breaches and stock flips.
Scheduled config ready to clone: DE pharmacy price & stock changes — daily delta.
The five patterns behind the delta engine are written up in Scraping a price is easy. Knowing it changed is the product.
If you hit a units bug like this one on another retail API, reply with the field names — the cents/euros pair shows up far more often than it should.
Top comments (0)