# Denmark Grocery Price Matrix (`studio-amba/dk-grocery-price-matrix`) Actor

One normalized feed of grocery products and prices across Foetex, Bilka, REMA 1000, SPAR, Netto and Lidl Denmark, matched to a common record shape and delivered as a single dataset.

- **URL**: https://apify.com/studio-amba/dk-grocery-price-matrix.md
- **Developed by:** [Studio Amba](https://apify.com/studio-amba) (community)
- **Categories:** E-commerce
- **Stats:** 1 total users, 0 monthly users, 87.1% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 1,000 result scrapeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Denmark Grocery Price Matrix

One normalized feed of grocery products across the major Danish supermarket chains: Foetex, Bilka, Bilka ToGo, REMA 1000, SPAR, MENY, Netto and Lidl. Give it search terms and it returns a single dataset with one common record shape per product, in DKK, with everyday and offer prices clearly labeled.

This actor does not scrape a website itself. It runs our eight Danish retailer scrapers in parallel and normalizes what comes back into one schema, so you get one feed and one API instead of stitching together eight different field layouts yourself.

### Why use this actor?

Danish grocery chains publish price data in different shapes — SPAR labels a multi-buy discount differently than REMA 1000 labels a weekly campaign, and two chains (Netto, Lidl) don't publish an everyday catalogue at all, only rotating weekly offers. Pulling all eight by hand and reconciling the field names is the tedious part. This actor does the fan-out and the normalization in one call and returns a flat table you can load straight into a spreadsheet, warehouse, or pricing dashboard.

### What it returns

Each row is one product at one retailer for one search term.

```json
{
    "query": "mælk",
    "retailer": "REMA 1000",
    "name": "Arla Skummetmælk",
    "brand": "Arla",
    "price": 12.60,
    "originalPrice": null,
    "currency": "DKK",
    "priceType": "everyday",
    "offerValidFrom": null,
    "offerValidTo": null,
    "ean": "5700426101415",
    "unitPrice": "12.6 DKK/l",
    "unitOfMeasure": "l",
    "packageSize": "1 LTR. / REMA 1000",
    "category": "Mejeri > Mælk",
    "inStock": null,
    "url": "https://shop.rema1000.dk/varer/123456",
    "imageUrl": "https://...",
    "productId": "123456",
    "scrapedAt": "2026-09-09T15:33:58.000Z"
}
```

An offer row (Netto, Lidl, or a temporary discount on any other chain) looks the same but with `priceType: "offer"` and, where the source publishes it, an `offerValidFrom`/`offerValidTo` window.

### Retailer coverage

| Retailer | Catalogue type | Notes |
|---|---|---|
| Foetex | Everyday shelf catalogue | Algolia-backed storefront search |
| Bilka | Everyday shelf catalogue | JSON-LD product pages; brand/category/unit price are only present on some listings — see Limitations |
| Bilka ToGo | Everyday shelf catalogue | Live. Salling's fresh-grocery webshop (bilkatogo.dk) — full dairy/produce/meat range, unlike Foetex/Bilka's dry-goods-only index |
| REMA 1000 | Everyday shelf catalogue + campaigns | Public search API, full field coverage |
| SPAR | Everyday shelf catalogue + campaigns | Reads one physical store's Spartid webshop (`sparStore` input, default Kastrup) — not every SPAR store runs one |
| MENY | Everyday shelf catalogue + campaigns, **per store** | Reads one physical store's grocery webshop (`menyStore` input, default Parkvej) — only 9 of MENY's 116 stores run one, see `docs/site-recon/meny-dk.md`. MENY is SPAR's sibling Dagrofa banner; the two chains price independently per store, sampled at ~80% divergence between stores |
| Netto | **Offers only** | No shop or product catalogue exists on netto.dk at all — every row is a time-boxed weekly leaflet offer (Tjek platform). Always `priceType: "offer"`. No EAN, no brand, no stock field in the source |
| Lidl | **Offers only** | Lidl DK's search API exposes only the current ~325-item weekly-offer rotation, not a static everyday catalogue. Always `priceType: "offer"` here |

Treat "everyday" rows as the shelf price you'd see walking into the store, and "offer" rows as a time-boxed campaign price — never assume an offer row is available outside its validity window.

### How to scrape Danish grocery data

1. Enter one or more Danish search terms in **Search Queries**, one per line (e.g. `mælk`, `kaffe`, `øl`).
2. Choose which **Retailers** to include. Leave all eight selected for full coverage, or narrow it down.
3. Set **Max Items Per Retailer** to control how many products each chain contributes per query.
4. Optionally set **SPAR Store** or **MENY Store** to a different store subdomain, or use **Category URLs** to browse a Bilka or Lidl category page instead of searching.
5. Run the actor. All selected retailers are queried in parallel per query, so adding retailers doesn't multiply run time.
6. Open the dataset. Filter by `retailer` to compare one chain, or by `priceType` to separate everyday shelf prices from time-boxed offers.

### Input

| Field | Type | Description |
|-------|------|-------------|
| `retailers` | array | Which of the eight chains to include (`foetex`, `bilka`, `rema1000`, `spar`, `meny`, `netto`, `lidl`, `bilkatogo`). Empty means all. |
| `searchQueries` | array | Danish search terms to price-check. Defaults to `["mælk", "kaffe", "øl"]`. |
| `categoryUrls` | object | Optional per-retailer category page URL, e.g. `{"bilka": "https://www.bilka.dk/mejeri"}`. Only Bilka and Lidl support this; when set for a retailer, `searchQueries` is skipped for that retailer. |
| `sparStore` | string | SPAR Spartid store subdomain to read (default `kastrup`). |
| `menyStore` | string | MENY grocery-webshop store subdomain to read (default `parkvej`). |
| `maxItemsPerRetailer` | integer | Max products considered per retailer per query (1-200, default 20). |
| `timeoutPerRetailerSecs` | integer | Max seconds to wait for each retailer scraper before treating it as failed (30-600, default 150). |
| `proxyConfiguration` | object | Proxy settings passed to the retailers that need one (Foetex, Bilka, REMA 1000, Lidl, Bilka ToGo). SPAR, MENY and Netto always run on the free pool — see FAQ. |

#### Example input

```json
{
    "retailers": ["foetex", "bilka", "rema1000", "spar", "meny", "netto", "lidl", "bilkatogo"],
    "searchQueries": ["mælk", "kaffe", "øl"],
    "sparStore": "kastrup",
    "menyStore": "parkvej",
    "maxItemsPerRetailer": 20,
    "proxyConfiguration": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"], "apifyProxyCountry": "DK" }
}
```

### Output

| Field | Type | Description |
|-------|------|-------------|
| `query` | string | The search term this row was fetched for, or `(category browse)` |
| `retailer` | string | Foetex, Bilka, Bilka ToGo, REMA 1000, SPAR Denmark, MENY Denmark, Netto Denmark or Lidl |
| `name` | string | Product name |
| `brand` | string | Product brand, when the retailer exposes one — see Retailer coverage |
| `price` | number | Effective current price in DKK — the campaign price when `priceType` is `offer`, the shelf price otherwise |
| `originalPrice` | number | Regular/everyday price, populated only when `price` reflects a current discount |
| `currency` | string | Always `DKK` |
| `priceType` | string | `everyday` or `offer` |
| `offerValidFrom` / `offerValidTo` | string | ISO timestamps the current offer price runs, when the source publishes them |
| `ean` | string | EAN/GTIN barcode, when published. Never present on Netto |
| `unitPrice` | string | Retailer-formatted per-unit price, e.g. `24,00 DKK/kg` |
| `unitOfMeasure` | string | Unit the unit price is expressed in, when parseable |
| `packageSize` | string | Pack size / quantity, free text |
| `category` | string | Product category |
| `inStock` | boolean/null | `true`/`false` when the source states availability, `null` when it doesn't — never guessed |
| `url` | string | Product (or leaflet) page URL |
| `imageUrl` | string | Product image URL |
| `productId` | string | Retailer-internal product or offer id |
| `scrapedAt` | string | ISO timestamp this row was fetched |

### FAQ

**Does this actor scrape the retailer websites itself?**
No. It calls our own Foetex, Bilka, Bilka ToGo, REMA 1000, SPAR, MENY, Netto and Lidl scrapers and normalizes their output into one schema. Each of those actors is also available separately on the Store.

**Why does Netto only show offer prices?**
Netto Denmark doesn't run an online shop or publish any product catalogue — its only structured price data is the weekly leaflet, a time-boxed campaign feed. There is no everyday shelf price to fall back to.

**Why does Lidl only show offer prices too?**
Lidl DK's product search API returns only the current weekly-offer rotation (roughly 325 items) rather than a static everyday catalogue, so every Lidl row here is treated as an offer for honesty, even though a handful carry a discount off a regular price.

**Why is `brand`/`category`/`unitPrice` sometimes empty on Bilka?**
Bilka's product pages carry this data in embedded JSON-LD, and not every listing includes the full set of optional fields — this is a source data-quality gap, not a bug in the matching. `name`, `price`, `ean` and `url` are always populated.

**Do I need my own proxy?**
No, for the retailers that need one (Foetex, Bilka, REMA 1000, Lidl, Bilka ToGo) proxy handling is passed through automatically with a sensible Danish residential default. SPAR, MENY and Netto read plain public JSON APIs with no anti-bot, so they always run on the free proxy pool regardless of your setting — forcing a paid proxy onto them would cost more for no benefit.

**Can I get just one physical SPAR or MENY store's catalogue?**
Yes — set `sparStore` (e.g. `erslev` for erslev.spar.dk) or `menyStore` (e.g. `saltum` for saltum.meny.dk). Not every physical SPAR or MENY store runs the webshop this actor reads from — see the SPAR Denmark Scraper and MENY Denmark Scraper READMEs for verified store lists.

**A retailer returned nothing for my query — did the run fail?**
Only if every retailer returned nothing. A single retailer or query combination failing (timeout, no matches) is logged and the run continues with everything else; check `KV store > RUN_SUMMARY` for a per-retailer breakdown of what succeeded.

### Pricing and cost

Cost estimate: this actor charges a start fee (per GB of memory) plus a small fee per result. The eight retailer scrapers it launches each bill their own pricing to your account on top, exactly as if you ran them directly — this meta adds its own charge per result on top of what the underlying scrapers already charge, the same disclosed double-charge model used by our other price-comparison metas. A three-query run across all eight chains at default settings returns 500-650 rows and costs roughly $1.50-3 depending on how many retailers you include; narrow `retailers` or `searchQueries` to control cost.

Usage cost for a run only settles once it reports `SUCCEEDED` — checking cost on a still-running or partially-finished run will show a number far below the final total.

### Limitations

- This is a combined feed, not a cross-retailer price match. Unlike some comparison tools, rows are not merged across retailers into one line per product — Danish own-brand goods (REMA 1000's milk vs Foetex's milk) are genuinely different SKUs with different EANs, so merging them would be guesswork. Filter or pivot on `name`/`ean` yourself if you need a side-by-side view.
- Netto and Lidl rows are always `priceType: "offer"` — see Retailer coverage.
- Bilka's optional fields (brand, category, unit price, package size) are inconsistently populated at the source.
- SPAR and MENY coverage is per physical store, not nationwide — set `sparStore`/`menyStore` to the store you care about. MENY and SPAR are sibling Dagrofa banners that price independently per store; do not assume their rows are comparable to a "national" price either.
- Data is scraped from public retailer sources and may change or move without notice.

### Related actors

This matrix is built on our standalone Danish grocery scrapers, each of which you can also run on its own for a full-catalogue pull:

- [Foetex Scraper](https://apify.com/studio-amba/foetex-scraper)
- [Bilka Scraper](https://apify.com/studio-amba/bilka-scraper)
- [Bilka ToGo Scraper](https://apify.com/studio-amba/bilkatogo-scraper)
- [REMA 1000 Denmark Scraper](https://apify.com/studio-amba/rema1000-dk-scraper)
- [SPAR Denmark Scraper](https://apify.com/studio-amba/spar-dk-scraper)
- [MENY Denmark Scraper](https://apify.com/studio-amba/meny-dk-scraper)
- [Netto Denmark Offers Scraper](https://apify.com/studio-amba/netto-dk-offers-scraper)
- [Lidl Scraper](https://apify.com/studio-amba/lidl-scraper)

We run this comparison as a managed service too: scheduled runs, a fixed SKU list, and delivery to your inbox, Google Sheets, or API, maintenance included. See [studioamba.dev/services](https://studioamba.dev/services/) or email <hello@studioamba.dev> for a free data sample.

### Known issues (Sep 2026)

- Bilka's own site search occasionally returns zero sitemap matches for a Danish search term containing special characters (e.g. "mælk") even though the chain does stock matching products — this is a search-matching gap in the standalone `bilka-scraper`, not in this matrix. The affected query/retailer combination is reported as failed in `RUN_SUMMARY` rather than silently dropped.
- Bilka ToGo and MENY were wired into this matrix 2026-09-09 (parity rule: every standalone we own in a vertical belongs in its multiscraper) and cloud-verified alongside the other six retailers.

# Actor input Schema

## `retailers` (type: `array`):

Which Danish chains to include. Leave all selected for full coverage. Netto and Lidl publish weekly offers only, not an everyday-price catalogue — see README.

## `searchQueries` (type: `array`):

Danish grocery search terms to price-check, one per line (e.g. 'mælk', 'kaffe', 'øl'). Each retailer is queried separately for every term.

## `categoryUrls` (type: `object`):

Optional per-retailer category page URL to browse instead of keyword search. Only Bilka and Lidl support this. Keys: 'bilka', 'lidl'. Example: {"bilka": "https://www.bilka.dk/mejeri", "lidl": "https://www.lidl.dk/c/tilbud/a10006065"}. When set for a retailer, searchQueries is ignored for that retailer.

## `sparStore` (type: `string`):

SPAR store subdomain slug to source the everyday catalogue from (e.g. 'kastrup' for kastrup.spar.dk). Not every physical SPAR store runs the Spartid webshop this actor reads from.

## `menyStore` (type: `string`):

MENY store subdomain slug to source the everyday catalogue from (e.g. 'parkvej' for parkvej.meny.dk). Only 9 of MENY's 116 physical stores run this grocery webshop — see the MENY Denmark Scraper README for the verified list.

## `maxItemsPerRetailer` (type: `integer`):

Maximum number of products to pull from each retailer, per search query. Higher values take longer and cost more.

## `timeoutPerRetailerSecs` (type: `integer`):

Maximum time to wait for each underlying retailer scraper to finish. A slow or failing retailer is skipped after this and reported in the run summary rather than blocking the whole run.

## `proxyConfiguration` (type: `object`):

Proxy settings passed to the retailer scrapers that need one (Foetex, Bilka, REMA 1000, Lidl, Bilka ToGo). SPAR, MENY and Netto use public APIs with no anti-bot and always run on the free pool regardless of this setting. Danish residential is recommended.

## Actor input object example

```json
{
  "retailers": [
    "foetex",
    "bilka",
    "rema1000",
    "spar",
    "meny",
    "netto",
    "lidl",
    "bilkatogo"
  ],
  "searchQueries": [
    "mælk",
    "kaffe",
    "øl"
  ],
  "categoryUrls": {},
  "sparStore": "kastrup",
  "menyStore": "parkvej",
  "maxItemsPerRetailer": 20,
  "timeoutPerRetailerSecs": 150,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "DK"
  }
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "retailers": [
        "foetex",
        "bilka",
        "rema1000",
        "spar",
        "meny",
        "netto",
        "lidl",
        "bilkatogo"
    ],
    "searchQueries": [
        "mælk",
        "kaffe",
        "øl"
    ],
    "categoryUrls": {},
    "sparStore": "kastrup",
    "menyStore": "parkvej",
    "maxItemsPerRetailer": 20,
    "timeoutPerRetailerSecs": 150,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "DK"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("studio-amba/dk-grocery-price-matrix").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "retailers": [
        "foetex",
        "bilka",
        "rema1000",
        "spar",
        "meny",
        "netto",
        "lidl",
        "bilkatogo",
    ],
    "searchQueries": [
        "mælk",
        "kaffe",
        "øl",
    ],
    "categoryUrls": {},
    "sparStore": "kastrup",
    "menyStore": "parkvej",
    "maxItemsPerRetailer": 20,
    "timeoutPerRetailerSecs": 150,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "DK",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("studio-amba/dk-grocery-price-matrix").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "retailers": [
    "foetex",
    "bilka",
    "rema1000",
    "spar",
    "meny",
    "netto",
    "lidl",
    "bilkatogo"
  ],
  "searchQueries": [
    "mælk",
    "kaffe",
    "øl"
  ],
  "categoryUrls": {},
  "sparStore": "kastrup",
  "menyStore": "parkvej",
  "maxItemsPerRetailer": 20,
  "timeoutPerRetailerSecs": 150,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "DK"
  }
}' |
apify call studio-amba/dk-grocery-price-matrix --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,studio-amba/dk-grocery-price-matrix"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/fjmCbYmxZyTMZdf2J/builds/KvAg13nYIU6z4VZVW/openapi.json
