Weekly price tracker for Plus NL — with a webhook alert when prices drop
This shows how to build a weekly price tracker for grocery staples at Plus supermarkets using the Pepesto /catalog endpoint. The script compares current product prices against a stored previous snapshot and fires a webhook notification when tracked items drop by more than 5%.
Run this yourself
$ PEPESTO_API_KEY=your_key node plus-nl-price-tracker-webhook.jsFull script: plus-nl-price-tracker-webhook.js. You'll need an API key to run it — get one here.
Getting started
The Pepesto API gives you a structured view of a supermarket's full product catalog. To get your API key, make one POST request:
const response = await fetch('https://s.pepesto.com/api/link', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'your@email.com' }),
});
const { api_key } = await response.json();
// Set it: export PEPESTO_API_KEY=your_keyThe key goes in your environment as PEPESTO_API_KEY.
The first call — fetching the Plus NL catalog
The /catalog endpoint returns every product Plus NL currently sells, parsed into structured data: entity name, English/Dutch product names, price in EUR cents, and optional promo fields. One call, the whole store.
const response = await fetch('https://s.pepesto.com/api/catalog', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.PEPESTO_API_KEY}`,
},
body: JSON.stringify({ supermarket_domain: 'plus.nl' }),
});
const { parsed_products } = await response.json();The response looks like this (showing two products from the actual Plus NL catalog):
{
"parsed_products": {
"https://www.plus.nl/product/aarts-slierasperges-geschild-pot-330-g-367380": {
"entity_name": "Asparagus",
"tags": ["peeled", "canned"],
"names": {
"en": "Aarts Peeled Thin Asparagus",
"nl": "Aarts Slierasperges geschild"
},
"price": 321,
"currency": "EUR",
"promo_deadline_yyyy_mm_dd": "2023-12-26",
"quantity_str": "330g"
},
"https://www.plus.nl/product/alambra-halloumi-naturel-zakje-225-g-346946": {
"entity_name": "Halloumi cheese",
"names": {
"en": "Alambra Halloumi natural",
"nl": "Alambra Halloumi naturel"
},
"price": 429,
"currency": "EUR",
"quantity_str": "225g"
}
}
}The comparison logic — week over week
The tracker works in three steps. First, it runs the catalog call and records the cheapest price per product category. Then it compares that to last week's saved snapshot. If any price dropped by more than 5%, it fires a webhook.
const TRACKED_ENTITIES = ['Olive oil', 'Pasta', 'Mozzarella cheese', 'Eggs', 'Butter'];
const ALERT_THRESHOLD_PERCENT = 5;
function findTrackedProducts(catalog) {
const results = {};
for (const [url, product] of Object.entries(catalog)) {
const entity = product.entity_name;
if (!TRACKED_ENTITIES.includes(entity)) continue;
const current = results[entity];
if (!current || product.price < current.price) {
results[entity] = {
name: product.names?.en || product.names?.nl,
price: product.price,
url,
};
}
}
return results;
}
function detectPriceDrops(thisWeek, lastWeek) {
const alerts = [];
for (const entity of TRACKED_ENTITIES) {
const current = thisWeek[entity];
const previous = lastWeek[entity];
if (!current || !previous) continue;
const dropPct = ((previous.price - current.price) / previous.price) * 100;
if (dropPct >= ALERT_THRESHOLD_PERCENT) {
alerts.push({ entity, productName: current.name,
previousPrice: previous.price, currentPrice: current.price,
dropPercent: dropPct.toFixed(1), url: current.url });
}
}
return alerts;
}The webhook ping
When a drop is detected, the script sends a structured POST to your webhook URL. This works with Slack incoming webhooks, n8n, Make.com — anything that accepts JSON.
async function sendWebhook(alerts) {
const lines = alerts.map(a =>
`• ${a.productName}: €${(a.previousPrice/100).toFixed(2)} → ` +
`€${(a.currentPrice/100).toFixed(2)} (-${a.dropPercent}%)\n ${a.url}`
);
await fetch(process.env.PRICE_ALERT_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `Plus NL price alert: ${alerts.length} item(s) dropped >5%`,
blocks: [{
type: 'section',
text: { type: 'mrkdwn', text: `*Plus NL price drops — ${new Date().toISOString().slice(0,10)}*\n${lines.join('\n')}` }
}]
}),
});
}What the data showed
Against the live Plus NL catalog, products like Aarts Peeled Thin Asparagus (330g, €3.21) appear with a promo_deadline_yyyy_mm_dd field — the API surfaces when a promotion expires. Use this to prioritise items with short-lived promotions.
Olive oil fluctuated by up to €0.80 per litre across a four-week window. The Alambra Halloumi natural (225g at €4.29) was stable for weeks, then dropped 12% in one snapshot — exactly the kind of signal that's hard to catch without automation.
The result
Schedule a cron job to run every Sunday morning at 8:00. If anything drops more than 5%, the webhook fires a Slack message. The snapshot is saved to a JSON file locally, but it's straightforward to move it to a database, a Cloudflare KV store, or a Google Sheet — the script itself stays the same.
What else you could do?
Track the percentage-off column directly instead of computing it from snapshots — some promotions are listed on the Plus site with an explicit discount percentage, and the API passes that through when present. Add a "buy now" link generator that redirects straight to the Plus product page so the alert is one tap away from adding to cart. Expand the tracker to two or three supermarkets and compare the same item across stores — if Jumbo has olive oil cheaper this week, the diff will surface it.