🇩🇪 REWE Example solution JavaScript /products API

REWE vs Coop CH: pricing one basket to check whether Germany really is 30% cheaper

Everyone living near the Swiss border knows to shop in Germany, and everyone quotes a different number for how much it saves. This puts one 30-item basket in front of both REWE and Coop CH, converts CHF to EUR, and reads off the difference.

Run this yourself

$ PEPESTO_API_KEY=your_key node rewe-germany-vs-switzerland-prices.js

Full script: rewe-germany-vs-switzerland-prices.js. You'll need an API key to run it — get one here.

Getting started

The plan: write one basket, parse it once, price it at REWE and Coop CH, convert CHF to EUR at a fixed rate, and compare. No scraping, no browser automation — three API calls and about 40 cents of credits.

js
// Get your key first:
const linkRes = 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 linkRes.json();
// export PEPESTO_API_KEY=your_key

One basket, two countries

A German own-label yoghurt and a Swiss own-label yoghurt are different products with different names, so there is nothing obvious to match them on. The trick is to let Pepesto do the matching: send the basket to /parse as plain generic names, keep the kg_token that comes back, and send that same token to /products for each chain. Both answer with the same list of items, each resolved to something that chain actually sells, and the item_name on each entry is what you pair on.

js
const BASKET = [
  'milk', 'butter', 'cheese', 'mozzarella cheese', 'eggs', 'yoghurt',
  'beef', 'ground pork', 'chicken', 'bacon', 'sausage',
  'tomatoes', 'onions', 'garlic', 'carrots', 'potatoes', 'lettuce', 'apples', 'bananas',
  'spaghetti', 'rice', 'flour', 'sugar', 'salt', 'olive oil', 'bread',
  'coffee', 'tea', 'orange juice', 'sparkling water',
];

/**
 * Turns the shopping list into a kg_token. Sending the same token to both
 * chains is what lets the two baskets line up item by item.
 */
async function parseBasket() {
  console.log(`Parsing a ${BASKET.length}-item basket...`);
  const response = await fetch(`${API_BASE}/parse`, {
    method: 'POST',
    headers,
    body: JSON.stringify({ recipe_text: BASKET.join('\n') }),
  });

  if (!response.ok) {
    const errorText = await response.text();
    throw new Error(`Pepesto API error — ${response.status}: ${errorText}`);
  }

  const { kg_token } = await response.json();
  return kg_token;
}

/**
 * Prices the basket at one chain.
 */
async function priceBasketAt(kgToken, domain) {
  console.log(`Pricing the basket at ${domain}...`);
  const response = await fetch(`${API_BASE}/products`, {
    method: 'POST',
    headers,
    body: JSON.stringify({ recipe_kg_tokens: [kgToken], supermarket_domain: domain }),
  });

  if (!response.ok) {
    const errorText = await response.text();
    throw new Error(`Pepesto API error for ${domain} — ${response.status}: ${errorText}`);
  }

  return response.json();
}

Thirty items is the limit for one /parse shopping list. Both chains priced 28 of them.

Here is what REWE returns for one item:

json
{
  "currency": "EUR",
  "items": [
    {
      "item_name": "Mozzarella cheese",
      "products": [
        {
          "product": {
            "product_name": "REWE Bio Organic Mozzarella 125g",
            "category": "Mozzarella cheese",
            "quantity": { "grams": 125 },
            "price": { "price": 129, "promotion": {} }
          },
          "num_units_to_buy": 1
        }
      ]
    }
  ]
}

And the same item at Coop, priced in CHF:

json
{
  "currency": "CHF",
  "items": [
    {
      "item_name": "Mozzarella cheese",
      "products": [
        {
          "product": {
            "product_name": "Prix Garantie Mozzarella Cheese 125g",
            "category": "Mozzarella cheese",
            "quantity": { "grams": 125 },
            "price": { "price": 80, "promotion": {} }
          },
          "num_units_to_buy": 1
        }
      ]
    }
  ]
}

Same item, same 125g pack, and Switzerland is cheaper — CHF 0.80 against €1.29. Read the product names and you can see why: REWE's cheapest mozzarella is its organic line, Coop's is its budget line. Note the currency field on the response rather than assuming it.

The matching logic

Each item comes back with several candidate products. Keep the cheapest one per store, so you are always comparing the best price each country offers for that item, and convert the Swiss side to euros as you go. The CHF rate is a fixed approximation — good enough for a snapshot, even if it drifts.

js
function buildCategoryIndex(productsData, currency) {
  const index = {};

  for (const item of productsData.items ?? []) {
    const cheapest = (item.products ?? [])
      .filter(p => typeof p.product?.price?.price === 'number')
      .sort((a, b) => a.product.price.price - b.product.price.price)[0];
    if (!cheapest) continue;

    const price = cheapest.product.price.price;
    const priceEurCents = currency === 'CHF' ? Math.round(price * CHF_TO_EUR) : price;

    index[item.item_name] = {
      name: cheapest.product.product_name || item.item_name,
      priceEurCents,
      originalPrice: price,
      originalCurrency: currency,
      url: cheapest.product.product_id || '',
    };
  }

  return index;
}

What the data showed

REWE was cheaper on 20 of the 28 matched items. The whole basket came to €50.89 in Germany against €59.19 in Switzerland — a gap of 14%. The median item was 16% cheaper in Germany. So the 30% figure is roughly double what this basket supports.

Where Germany wins, it wins on fresh food. Tomatoes €1.19 against €2.55, oranges €1.39 against €2.81, chicken breasts €3.89 against €5.15, eggs €2.29 against €3.07. Meat and pantry staples went to REWE across the board without a single exception.

Switzerland's wins nearly all come from Coop's Prix Garantie budget line, which has no real German equivalent at that price point. Mozzarella is CHF 0.80 against €1.29, garlic CHF 0.52 against €1.09. The biggest single gap in the whole table runs Switzerland's way: nine litres of sparkling mineral water, six 1.5-litre bottles of Prix Garantie, at CHF 1.56 against €5.34 for the same nine litres at REWE. Identical volume, three and a half times the price.

Where the numbers mislead

Two of the results are matching artefacts and should not be quoted.

Ask for potatoes and REWE's cheapest match is a bag of Lorenz Crunchips Cheese & Onion at €1.99, against a genuine kilo of potatoes at Coop for CHF 1.10. Crisps are potatoes in the sense that matters to a matcher and in no sense that matters to a shopper.

Bananas look like a German win — €0.40 against €1.46 — until you read quantity. REWE's match is a single organic banana of about 200g; Coop's is a 1.2kg bunch. Per kilo that is €2.00 in Germany against €1.22 in Switzerland, so the sign flips entirely. Divide by quantity.grams before believing anything sold loose.

The result

One /parse and two /products calls, currency conversion, and a scorecard, in a few seconds and for about 40 cents. Germany is genuinely cheaper, and the gap on this basket is 14% — real, worth the drive if you are already close to the border, and nothing like the 30% people repeat.

Next steps

To make it more rigorous, normalise everything to price per kilo or per litre from the quantity object before comparing, rather than trusting the sticker price. That alone fixes the banana problem. Filtering the candidate list by pack size before picking the cheapest would fix the crisps.

What else you could do?

Add Migros to the comparison as a second Swiss store — useful for checking whether REWE is closer to Coop or Migros on price for the same items. Run the comparison weekly and track drift over time — CHF has been strengthening, which changes the effective gap. Add a filter for promo: true on the REWE side to show best-case Germany prices, since REWE runs significant weekly promotions.

Links

Ready to build?

Start comparing German and Swiss grocery prices

Fetch live REWE and Coop CH catalogs in parallel and settle the cross-border price debate with data.

27supermarkets 13countries 1schema Instant access