German Supermarket Offers — Aldi, Penny, Edeka, Lidl, REWE
Pricing
from $2.00 / 1,000 offers
German Supermarket Offers — Aldi, Penny, Edeka, Lidl, REWE
Weekly offers and prices from 6 German supermarket chains (Aldi Süd, Aldi Nord, Penny, Edeka, Lidl, REWE) in one unified schema: price, old price, discount, unit, validity dates, image. Keyword filter, per-chain limits. Pay per offer.
Pricing
from $2.00 / 1,000 offers
Rating
0.0
(0)
Developer
Nerdrx
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Share
German Supermarket Offers
Unofficial weekly-offer scraper for six German supermarket chains. Not affiliated with, endorsed by, or officially connected to ALDI SÜD, ALDI Nord, PENNY, EDEKA, Lidl, REWE, Kaufland, or Netto Marken-Discount. It reads each chain's own public offers page or the JSON endpoint that page itself calls — the same data anyone sees by opening the chain's "Angebote" page in a browser.
What is German Supermarket Offers?
One Apify actor run that collects this week's advertised offers from ALDI SÜD, ALDI Nord, PENNY, EDEKA, Lidl and REWE and hands them back as a single flat dataset with one row per offer — instead of you writing and maintaining six separate scrapers against six completely different stacks (a REST API, a Next.js payload, a Magnolia REST fragment, a legacy offers service, a leaflet API, and server-rendered HTML).
What can German Supermarket Offers do?
- Pull the current week's offers from all six chains (or any subset) in one run.
- Give every offer the same shape: price, previous price, discount percent, pack size, base price, validity window, category, image.
- Reconstruct what a chain leaves implicit — e.g. EDEKA publishes a discount but no previous price, so the previous price is computed; ALDI SÜD publishes a previous price but the discount only sometimes.
- Filter by keywords: OR-matched, case-insensitive, and umlaut/ß-insensitive —
kaesematchesKäseand vice versa,weissbiermatchesWeißbier. - Target a specific store for the chains whose prices are regional (PENNY, EDEKA, ALDI SÜD).
- Keep going when one chain breaks: a failing chain is logged loudly as an error and the run continues with the rest. Individual requests retry twice with backoff and randomized realistic browser headers.
- Interleave the chains round-robin, so a run capped by
maxItemsstill contains offers from every chain rather than filling up with whichever chain ran first.
What data does it extract?
| Field | Type | Description |
|---|---|---|
id | string | Stable, chain-scoped offer id, e.g. edeka:500000377. |
chain | string | Chain slug: aldi-sued, aldi-nord, penny, edeka, lidl, rewe. |
chainName | string | Human-readable chain name, e.g. ALDI SÜD. |
title | string | Product name as advertised. |
brand | string | null | Brand — only where the chain publishes it separately from the title. |
description | string | null | Advertising sub-line: origin, variety, pack details. |
price | number | null | Offer price in EUR. |
priceOld | number | null | Reference/previous price in EUR, where published or derivable. |
discountPercent | integer | null | Discount in percent, as stated by the chain or derived from price/priceOld. |
unit | string | null | Pack size, e.g. 500 g, 0,75 l, Stück. |
pricePerUnit | string | null | Base price exactly as printed, e.g. 1 kg = 3,18 €. |
validFrom | string | null | First day of the promotion (ISO date). |
validTo | string | null | Last day of the promotion (ISO date). |
category | string | null | Category the chain files the offer under. |
imageUrl | string | null | Product image URL. |
storeId | string | null | The store these prices belong to, for the store-specific chains. |
sourceUrl | string | A page a human can open to see this offer. |
scrapedAt | string | ISO-8601 timestamp this item was scraped. |
Chains and sources
| Chain | Slug | Source type | Endpoint / page | Store id needed? | robots.txt |
|---|---|---|---|---|---|
| ALDI SÜD | aldi-sued | JSON API | api.aldi-sued.de/v3/product-search (category Wochenangebote) | Optional (servicePoint) | Allowed — aldi-sued.de/robots.txt only disallows /tools and /*?q= |
| ALDI Nord | aldi-nord | Embedded JSON in HTML | aldi-nord.de/angebote.html → __NEXT_DATA__ → apiData → OFFER_GET | No (national) | Allowed — /angebote.html is not in the disallow list |
| PENNY | penny | HTML + JSON API | penny.de/angebote (week + categories) → /.rest/offers/by-category/{week}/{category}?region=…, markets from /.rest/market | Yes (wwIdent; a default market is picked otherwise) | Allowed — penny.de/robots.txt is Allow: / |
| EDEKA | edeka | JSON API | edeka.de/eh/service/eh/offers?limit=…&marketId=… | Optional (marketId; 1 = national list) | Allowed — /eh/ is not disallowed |
| Lidl | lidl | HTML + JSON API | lidl.de/c/online-prospekte/s10005610 → endpoints.leaflets.schwarz/v4/flyer?flyer_identifier=… | No (national leaflet, region 0) | Allowed — none of Lidl's disallow rules cover /c/ or /l/prospekte/ |
| REWE | rewe | Server-rendered HTML | rewe.de/angebote/nationale-angebote/<category>/ | No (national offers only) | Explicitly allowed — rewe.de/robots.txt lists every /angebote/nationale-angebote/… path under Allow: while disallowing /restservices/ |
Chains that are deliberately not included
| Chain | Why it was dropped |
|---|---|
| Kaufland | kaufland.de/angebote/aktuelle-woche.html serves a Cloudflare "Verifizierung erforderlich" bot challenge instead of the page. Getting past it would mean defeating bot detection, which this actor does not do. |
| Netto Marken-Discount | Offers are only rendered after a store is chosen in a session, the site runs Akamai Bot Manager, and netto-online.de/robots.txt disallows /INTERSHOP/ (its data endpoints) except for a short whitelist that does not include offers. |
The actor's title mentions Netto and Kaufland because that is the market it addresses; the dataset does not contain them. See NOTES.md.
These are the chains' own public pages and the endpoints those pages call, not a paid data license. Endpoint shapes, selectors and category lists can change without notice — see NOTES.md for exactly what was live-verified and when.
How to use
- (Optional) Pick which
chainsto run — all six by default. - (Optional) Add
keywords, e.g.["Käse", "Kaffee", "Bio"]. Leave empty to get every offer. - (Optional) Set
storeIdsfor the regional chains, e.g.{"penny": "4030085", "edeka": "10000327"}. - (Optional) Adjust
maxItems(total) andmaxPerChain. - Run it. Results land in the run's dataset; the Overview view shows chain, product, price, discount and validity as a table.
Scheduling once a week — early on the day the new week starts (Monday for most chains) — gives you a clean weekly price history. Enable proxyConfiguration if you run it often.
How much does it cost?
This actor is pay per event:
| Event | When it is charged | Planned price |
|---|---|---|
offer | Once per offer successfully pushed to the dataset. | $0.002 |
A default run capped at maxItems: 500 therefore costs at most $1.00 plus Apify platform compute (the actor is HTTP-only and runs comfortably in 256–512 MB, so compute is a few cents). Nothing is charged for offers that are filtered out by your keywords, for chains that fail, or for the store/category lookup requests.
If you set a maximum charge on the run and it is reached, the actor stops pushing further offers, logs a warning, and exits cleanly — you are never charged for an item that did not make it into the dataset.
Input
| Field | Type | Default | Description |
|---|---|---|---|
chains | array of enum | all six | Which chains to scrape. |
keywords | array of string | [] | Keep only offers matching at least one keyword (umlaut-insensitive substring match on title, brand, description, category). Empty = keep everything. |
storeIds | object | {} | Per-chain store id, e.g. {"penny": "4030085"}. PENNY takes a wwIdent, EDEKA a market id, ALDI SÜD a servicePoint. ALDI Nord, Lidl and REWE publish national offers and ignore it. |
maxItems | integer | 500 | Maximum offers written to the dataset across all chains. |
maxPerChain | integer | 200 | Maximum offers taken from each chain before keyword filtering. |
proxyConfiguration | object | none | Optional proxy for all outbound requests. |
{"chains": ["aldi-sued", "penny", "edeka", "rewe"],"keywords": ["Käse", "Kaffee"],"storeIds": { "penny": "4030085" },"maxItems": 300,"maxPerChain": 150}
Output (sample)
{"id": "aldi-sued:000000000000230773","chain": "aldi-sued","chainName": "ALDI SÜD","title": "Mandarinen 750 g","brand": "NATUR LIEBLINGE","description": null,"price": 1.49,"priceOld": 1.79,"discountPercent": 16,"unit": "0,75 kg","pricePerUnit": "1,99 €/1 kg","validFrom": "2026-08-31","validTo": "2026-09-05","category": "Frischeprodukte im Angebot","imageUrl": "https://dm.emea.cms.aldi.cx/is/image/aldiprodeu/product/jpg/scaleWidth/600/706b9aa7-f1fb-484e-b14a-898a64e945f8","storeId": null,"sourceUrl": "https://www.aldi-sued.de/produkte/wochenangebote/k/1588161426582123","scrapedAt": "2026-09-02T10:00:00.000Z"}
FAQ & support
Is this official? No. This is an unofficial scraper. It is not affiliated with or endorsed by any of the chains named here. Use the data for personal price comparison and research; check each chain's terms before redistributing it.
Why aren't Kaufland and Netto in the output?
Kaufland answers with a Cloudflare bot challenge and Netto sits behind Akamai Bot Manager with its data endpoints disallowed in robots.txt. Rather than work around either, they are excluded. See the table above and NOTES.md.
Why do some offers have no priceOld or discountPercent?
Because the chain does not publish one. Lidl's leaflet feed and REWE's national offer tiles state only the offer price; a "Knaller" or "Aktion" badge is not a reference price, so inventing one would be misleading. EDEKA publishes the discount without the old price, so priceOld there is derived from price and the stated discount — treat it as a reconstruction, not as a figure EDEKA printed.
Are the prices what I will pay in my local store?
Only approximately. ALDI Nord, Lidl and REWE (national) offers are country-wide. PENNY, EDEKA and ALDI SÜD prices vary by region or store — pass a storeId for those to get the list for a specific store. App-only prices (EDEKA App, Lidl Plus, PENNY Coupons) are not the shelf price and are not separated out in this dataset.
How accurate are validFrom / validTo?
ALDI Nord, EDEKA and Lidl publish machine-readable dates and those are passed through. PENNY's dates are derived from its week id plus the promotion period ("ab Montag" = Mo–Sa, "ab Donnerstag" = Do–Sa). ALDI SÜD publishes no dates on its API at all, so the current Monday–Saturday week is used. REWE's come from the "Diese Woche" tab text. NOTES.md lists which is which.
REWE returned fewer categories than usual.
REWE rate-limits by IP and can answer 403 for some categories when the run comes from a datacenter address. Each request already retries twice with backoff and browser-realistic headers; if categories are still blocked the run logs one warning naming them and returns the rest. Enabling proxyConfiguration with residential proxies is the reliable fix.
Why does EDEKA return only ~20 offers?
Because that is the whole national list. EDEKA's service reports the total result-set size in anzahl, and it reads 20 regardless of the limit you send — there is no paging to exhaust. Pass a real storeIds.edeka market id (from EDEKA's market search) to get that market's larger list.
Something broke / a chain returns nothing. Chain sources change. Open an issue with the run id and the log line for the failing chain. Individual chain failures never abort the run — you still get the other chains' data.