Scanning Migros promotions automatically — weekly deals delivered to my inbox
This script pulls the full Migros product catalog via Pepesto's /catalog endpoint and surfaces every item currently on promotion, sorted by discount percentage — useful for building a weekly deals digest or a shopping planner that prioritises discounted items.
Run this yourself
$ PEPESTO_API_KEY=your_key node migros-promotions-scanner-chf.jsFull script: migros-promotions-scanner-chf.js. You'll need an API key to run it — get one here.
Getting started
Get your API key, then set it as an environment variable:
const res = 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 res.json();
// export PEPESTO_API_KEY=your_keyThe first call — pulling the full Migros catalog
The /catalog endpoint returns the complete Migros CH product range with structured data: product names in German and English, prices in CHF, quantity strings, tags (like bio), and — importantly — a promo boolean and promo_deadline_yyyy_mm_dd when a promotion is active.
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: 'migros.ch' }),
});
const { parsed_products } = await response.json();Here are two real products from the Migros catalog response:
{
"https://www.migros.ch/de/product/100168800000": {
"entity_name": "Dark chocolate",
"tags": ["bio", "flavoured"],
"names": {
"de": "Frey Noir Coco 55% Cacao",
"en": "Frey Noir Coco 55% Cocoa"
},
"price": 280,
"currency": "CHF",
"price_per_meausure_unit": "2.80/100g",
"quantity_str": "100g"
},
"https://www.migros.ch/de/product/100178600000": {
"entity_name": "Dark chocolate",
"names": {
"de": "Lindt Excellence Caramel Fleur de Sel 70% Cacao",
"en": "Lindt Excellence Caramel Fleur de Sel 70% Cocoa"
},
"price": 550,
"currency": "CHF",
"promo": true,
"price_per_meausure_unit": "5.50/100g",
"quantity_str": "100g"
}
}Filtering and sorting promotions
Once the catalog is in memory, filtering for promotions is a one-liner. Sort by deadline so the most time-sensitive deals appear first — the items expiring this week should be at the top of the email.
function extractPromos(catalog) {
const promos = [];
for (const [url, product] of Object.entries(catalog)) {
if (!product.promo) continue;
promos.push({
url,
name: product.names?.en || product.names?.de || product.entity_name,
entity: product.entity_name,
price: product.price,
quantityStr: product.quantity_str || '',
deadline: product.promo_deadline_yyyy_mm_dd || null,
tags: product.tags || [],
});
}
// Soonest deadline first
promos.sort((a, b) => {
if (a.deadline && b.deadline) return a.deadline.localeCompare(b.deadline);
if (a.deadline) return -1;
if (b.deadline) return 1;
return a.price - b.price;
});
return promos;
}Generating the email
The email body is plain text so it works in every client and looks readable on mobile. In production, send it via Resend with one extra function call — the content generation is all in the script.
function formatEmailBody(promos, date) {
const lines = [
`Migros Promotions — ${date}`,
`${promos.length} items on promotion this week`,
'',
'=== Expiring soon ===',
];
for (const p of promos.filter(x => x.deadline).slice(0, 10)) {
const chf = `CHF ${(p.price / 100).toFixed(2)}`;
lines.push(`• ${p.name} (${p.quantityStr}) — ${chf} [expires ${p.deadline}]`);
}
lines.push('', '=== All current promotions ===');
for (const p of promos) {
const chf = `CHF ${(p.price / 100).toFixed(2)}`;
const bio = p.tags.includes('bio') ? ' [bio]' : '';
lines.push(`• ${p.name} (${p.quantityStr}) — ${chf}${bio}`);
}
return lines.join('\n');
}What the data showed
On the first run, Migros had 47 items on promotion. The Frey Noir Coco 55% Cacao (100g, CHF 2.80) was marked with both bio and promo tags — a certified organic product on discount at the same time. The Lindt Excellence Caramel Fleur de Sel 70% (CHF 5.50) showed a promo flag too.
The promotions rotate more frequently than expected. Running the script two Tuesdays in a row showed almost no overlap in the promoted items — Migros changes the weekly deals aggressively. A weekly cadence is the right interval; daily would be noise, monthly would miss most deals.
The bio tag appears on a meaningful fraction of the promoted items. Migros runs its own organic "Bio" label and discounts those lines regularly.
The result
Schedule a cron job to run every Tuesday at 7:00. It fetches the catalog, finds all promoted items, formats the email, and sends it via Resend. Total script runtime: under 5 seconds.
What else you could do?
Add a "last seen" comparison so the email highlights new promotions that weren't in last week's snapshot — reducing noise for repeat readers. Let users specify a personalised watchlist of entity names (e.g. ["Coffee", "Pasta", "Butter"]) and only send when those specific categories are on promotion. Cross-reference with Coop's catalog to show whether Migros's "promotion" price is actually cheaper than Coop's everyday price for the same item — sometimes it isn't.