Promo Pulse — Belgian Supermarket Promo Tracker
Pricing
from $10.00 / 1,000 promo/price events
Promo Pulse — Belgian Supermarket Promo Tracker
Scheduled meta-scraper that tracks a fixed shelf of products at Lidl, Action and Colruyt Belgium, diffs prices against the previous run, and emits only promo START/STOP and price-change events — not a full re-export.
Pricing
from $10.00 / 1,000 promo/price events
Rating
0.0
(0)
Developer
Studio Amba
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
5 days ago
Last modified
Categories
Share
Promo Pulse
Tracks a fixed shelf of products at Lidl, Action and Colruyt Belgium on a schedule, and tells you only what changed since the last run — promo started, promo ended, price dropped, price restored. Not a full price re-export.
Why use this actor?
Most retail scrapers give you a full snapshot every time you run them — hundreds of products, most of which haven't changed since yesterday. If you actually want to know when a promotion starts or ends, you end up doing the diffing yourself: storing yesterday's prices somewhere, comparing them to today's, and figuring out which rows actually matter. This actor does that diffing for you.
It's a meta-actor: under the hood it calls our own Lidl, Action and Colruyt scrapers, tracks the same set of products every run (the "shelf"), and remembers each product's price and promo status in a persistent, named key-value store — separate from the run's own storage, so it survives across scheduled runs, not just within one. Every run compares the current state to the stored state and only pushes rows for products that actually changed.
Useful for price-monitoring tools, deal-alert bots, competitor pricing research, or just watching whether a specific product's promo depth is worth waiting for.
How to scrape supermarket promotion data (track promotions run-over-run)
- Pick your stores (or leave the field empty for all 3: Lidl, Action, Colruyt).
- Run it once. The first run has nothing to compare against, so it seeds its memory with the current price and promo status of every tracked product and pushes one status row per store ("baseline seeded") — no change events yet, because there's no prior run to diff against.
- Schedule it (see below). Every run after the first compares the current shelf state to what was stored last time and pushes one row per product that changed: a promo starting, a promo ending, a price dropping, or a price going back up.
- A run where nothing changed still isn't an empty dataset — it pushes a single "zero-delta" status row per healthy store, so you can tell the difference between "nothing changed" and "the run silently failed."
Scheduling
This actor's whole value is in running on a schedule, not as a one-off. In the Apify Console:
- Open the actor, go to Schedules → Create new schedule.
- Every 6-12 hours is a reasonable cadence — grocery/retail promos don't change more often than that, and running more frequently just burns compute without finding more events. Twice a day (e.g.
0 8,20 * * *) catches most weekly-cycle promo changes without over-polling. - Leave
resetStateoff (it's off by default). That flag wipes the actor's memory and would make every scheduled run look like a first run, which defeats the entire point of a diff feed. Only use it manually for testing. - Point the schedule's output at an integration (Zapier, Make, a webhook, a Slack channel) that fires per new dataset item, so promo events land wherever you actually watch them, the same run they're detected.
Input
| Field | Type | Required | Description |
|---|---|---|---|
stores | Array | No | Which stores to track: lidl, action, colruyt. Empty = all 3. |
maxResultsPerQuery | Integer | No | Max products fetched per tracked search query per store (default 15). Action and Colruyt track 4 queries each, so their shelf is roughly 4x this value; Lidl tracks 1 (see Limitations). |
timeoutPerSourceSecs | Integer | No | Max wait per underlying store scraper call before it's marked failed and skipped (default 150). |
proxyConfiguration | Object | No | Residential proxy passed through to the child scrapers, scoped to Belgium. |
resetState | Boolean | No | Testing only — wipes the stored shelf state for every store before this run, so every store re-seeds instead of diffing. Never use this on a real schedule. |
Output
Each row is either a genuine change event or a run-status placeholder.
| Field | Type | Example | Description |
|---|---|---|---|
eventType | String | promo_started | One of promo_started, promo_ended, price_dropped, price_restored, or run_status. |
productName | String | EVERYDAY Volle melk brik 1L | Product name, or a status message on a run_status row. |
ean | String | "" | Product barcode. Currently always empty — see Limitations. |
store | String | Colruyt | Lidl, Action, or Colruyt. |
oldPrice | Number | null | 1.11 | Price observed the previous run. |
newPrice | Number | null | 0.89 | Price observed this run. |
depthPct | Number | null | -20 | Run-over-run price change, negative for a drop, positive for an increase. |
promoText | String | Price dropped 20% | Human-readable summary of the event. |
url | String | https://www.colruyt.be/nl/producten/17691 | Product page — the stable identity key used to match a product across runs. |
detectedAt | String (ISO) | 2026-07-21T13:15:21.552Z | Timestamp of this run. |
Example output
[{"eventType": "price_dropped","productName": "EVERYDAY Volle melk brik 1L","ean": "","store": "Colruyt","oldPrice": 1.11,"newPrice": 0.89,"depthPct": -20,"promoText": "Price dropped 20%","url": "https://www.colruyt.be/nl/producten/17691","detectedAt": "2026-07-21T13:15:21.552Z"},{"eventType": "promo_ended","productName": "BONI halfvolle melk PET 50cl","ean": "","store": "Colruyt","oldPrice": 0.45,"newPrice": 0.65,"depthPct": 42.9,"promoText": "Promo ended — price restored","url": "https://www.colruyt.be/nl/producten/1599","detectedAt": "2026-07-21T13:15:21.552Z"}]
Store coverage and how the shelf is defined
Each store tracks a fixed set of search queries every run — the same queries, run after run — so the same products keep recurring and can actually be diffed. Rotating the queries would make most products look "new" every time, which defeats the diff.
- Lidl (
lidl-scraper): 1 query. Verified live that Lidl's own search API doesn't filter by thequerytext — "chocolade", "koffie" and "wijn" all returned the identical top listing (confirmed: "9288 total found" for every query, the full catalog size, not a filtered count). In practice this returns Lidl's own rotating top listing, dominated by their non-food "weekly offer" items — actually a decent fit for a promo tracker, since that catalog rotates on a fixed weekly cycle. Flagged as a site/actor-level quirk worth healing separately; not something this actor can fix by choosing better keywords. - Action (
action-scraper): 4 queries (kaars,opberger,speelgoed,decoratie). Confirmed the search actually filters — each query returns visibly different, relevant products. - Colruyt (
colruyt-scraper): 4 queries (melk,brood,kaas,pasta). Confirmed filtering works correctly.
Only these 3 were wired. Other grocery children in the fleet (Tesco/Sainsbury's/Asda/Morrisons, Rewe) were left out for this build — Tesco and Sainsbury's are UK/private and would mean mixing currencies and a separate schedule cadence for no real benefit over the Belgian set; Rewe is German and adds a second country's worth of complexity without a second country's worth of value yet. Belgium was chosen because it gave 3 solid, cloud-verified sources with real, distinct promo signals rather than stretching to more, flakier ones.
Limitations
- No EAN/barcode. None of the three wired source scrapers (Lidl, Action, Colruyt) currently return a barcode field. The product URL is used as the stable identity key across runs instead — it's reliable, just not a universal barcode.
- Colruyt has no "regular price" baseline. Colruyt's API returns the current selling price and a flag for whether it's currently a promo price, but never the pre-discount regular price.
depthPctfor Colruyt promo events is computed from the run-over-run price delta, not from a true "% off list price" figure. Lidl and Action do expose a real pre-discount price, so theirpromoTextincludes the actual discount percentage when available. - Lidl's search doesn't filter by keyword (see above) — the tracked shelf is whatever Lidl's own top listing returns, not specifically the query text.
- A new product appearing on the shelf is not itself an event. If a product wasn't in the previous run's snapshot (e.g. it just entered stock, or a query started surfacing it), this run only adds it to the baseline — it won't emit an event until a second run sees a change from that baseline.
- First run per store never emits change events, only a baseline-seeded status row — there's nothing to diff against yet.
Cost estimate
Priced per detected event, not per product scanned — a quiet run costs almost nothing. At $0.01/event plus a $0.01 run-start fee, a run that finds 10 promo changes across all 3 stores costs about $0.11.
Related Scrapers
- Lidl Scraper — full product/price scraper this actor calls under the hood
- Action Scraper — full product/price scraper this actor calls under the hood
- Colruyt Scraper — full product/price scraper this actor calls under the hood
- UK Grocery Price Matrix — cross-retailer price comparison, same meta-scraper family
- EU Fresh Company Radar — same delta-feed design pattern, applied to company registrations instead of prices