# Lidl Scraper — European Grocery Prices & Weekly Offers (`studio-amba/lidl-scraper`) Actor

Scrape product data from Lidl's online store. Extract grocery items, weekly offers, prices, categories, and product details from Lidl across 30+ European countries. No login or cookies required.

- **URL**: https://apify.com/studio-amba/lidl-scraper.md
- **Developed by:** [Studio Amba](https://apify.com/studio-amba) (community)
- **Categories:** E-commerce
- **Features:** Limited permissions
- **Stats:** 58 total users, 6 monthly users, 99.6% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 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.

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

## Lidl Scraper — European Grocery Products, Prices & Weekly Offers

Know current prices, stock, and weekly discounts on any product across **Lidl's 30+ European country stores** — without checking the site by hand. Extract product names, prices, brands, weekly offers, discounts, stock status, and category information. No login or cookies required.

### What does Lidl Scraper do?

This scraper uses Lidl's internal search API to collect structured product data at scale. It supports all Lidl country stores and returns clean, structured JSON output.

**Key features:**

- Search products by keyword across any Lidl country store
- Extract current prices and original/strikethrough prices for discount tracking
- Get discount percentages for weekly offers and promotions
- Monitor stock availability for any product
- Support for 30+ European countries with a single actor
- Sort by relevance, price, discount, rating, or popularity

### Use cases

- **Price monitoring** — Track grocery prices over time across Lidl stores
- **Weekly offers tracking** — Get notified about new Lidl deals and discounts
- **Competitor analysis** — Compare Lidl prices with other European grocery chains
- **Market research** — Analyze product assortment across different countries
- **Deal hunting** — Find the biggest discounts automatically with discount sorting

### Supported countries

Belgium, Netherlands, Germany, France, Austria, Poland, Italy, Spain, United Kingdom, Czech Republic, Portugal, Denmark, Sweden, Finland, Hungary, Romania, Bulgaria, Greece, Croatia, Slovenia, Slovakia, Lithuania, Latvia, Estonia, Ireland, Luxembourg, Switzerland, Cyprus, Malta, Serbia.

### Input parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `searchQuery` | string | Search term (e.g. "chocolade", "laptop"), a Lidl product number (e.g. "10030861") or an EAN. Leave empty for all products. Lidl sends a few words (e.g. "angebote", "folder") to a brochure page instead of a product list; the run then fails and the status message names that page. |
| `country` | string | Country code (BE, NL, DE, FR, etc.). Default: BE. |
| `categoryUrl` | string | Optional. A Lidl category page (URL ending in `/h<number>`, e.g. `https://www.lidl.be/h/nl-BE/badkamer/h10067553`) or offers page (ending in `/a<number>`). Replaces the search query, and the shop in the URL sets the country. Overview pages ending in `/s<number>` have no product list and fail with a message. |
| `sort` | string | Sort order: relevance, price, discount, newest, rating, best selling. |
| `maxResults` | number | Maximum products to scrape (default: 100, max: 10,000). |
| `proxyConfiguration` | object | Proxy settings (datacenter proxies usually work). |

### Output example

```json
{
    "name": "MILBONA",
    "brand": "MILBONA",
    "price": 8.0,
    "currency": "DKK",
    "originalPrice": null,
    "discountPercentage": null,
    "category": "Food",
    "imageUrl": "https://www.lidl.dk/assets/gcp27d0747cb279410183ee4865de4d2d0a.png",
    "description": null,
    "inStock": false,
    "productId": "11000287",
    "country": "DK",
    "url": "https://www.lidl.dk/p/milbona/p11000287",
    "scrapedAt": "2026-09-09T14:47:24.585Z",
    "unitPrice": "Pr. l 32,00",
    "packaging": "250 ml",
    "ean": "4335619016194",
    "offerValidFrom": "2026-08-26T11:22:06.225Z",
    "offerValidTo": "2026-09-12T21:59:59Z"
}
```

### Output fields

| Field | Type | Description |
|-------|------|-------------|
| `name` | string | Full product name including brand |
| `brand` | string | Brand name (SILVERCREST, LIVARNO, PARKSIDE, etc.) |
| `price` | number | Current selling price. For a Lidl Plus app deal this is the Lidl Plus price (see `priceType`). Null when Lidl has not published a price for the item yet (its offer window hasn't started, or has ended) |
| `priceType` | string | `regular` for the shelf price, `lidlPlus` when the only price Lidl publishes for the item is the Lidl Plus app price, null when there is no price |
| `lidlPlusPrice` | number | The Lidl Plus app price, where the item has one (null otherwise) |
| `lidlPlusText` | string | The Lidl Plus deal text as Lidl shows it (e.g. "2 + 1 GRATIS", "3 VOOR") |
| `queryInTitle` | boolean | Search runs only: true when every word of `searchQuery` appears in the product name. Lidl's own search also returns products that do not match the word (a search for "chocolade" in Belgium returns a cucumber and wooden toys among the chocolate). Rows stay in Lidl's order; filter on this field to keep only name matches. Null for `categoryUrl` runs |
| `currency` | string | Currency code (EUR, GBP, DKK, etc.) |
| `originalPrice` | number | Price before discount (null if no discount) |
| `discountPercentage` | number | Discount percentage (null if no discount) |
| `unitPrice` | string | Raw unit-price text as Lidl shows it (e.g. "Pr. l 32,00" = per litre, in Denmark). In some countries this field instead holds a multipack quantity range (e.g. "2 x 180 g - 2 x 500 g") \u2014 Lidl doesn't separate the two structurally |
| `packaging` | string | Package size/quantity (e.g. "250 ml", "45 g"), where Lidl exposes it as a separate field from the unit price |
| `ean` | string | EAN/GTIN barcode, where Lidl exposes one. Availability tracks product type, not country: present on Danish food items and on Belgian non-food/durable items (own-brand tools, kitchenware, toys), absent on the Belgian grocery/snack items sampled in the same run. Treat it as opportunistic per product, not a guaranteed field for any given country or category. Lidl UK (`GB`) publishes no EAN at all: the barcode list on its product pages is empty for food and non-food alike (checked October 2026), so `ean` is always null there |
| `offerValidFrom` / `offerValidTo` | string | ISO 8601 validity window for the current price, where Lidl publishes one (typically weekly offers) |
| `category` | string | Full category path |
| `imageUrl` | string | Main product image URL |
| `description` | string | Product description (HTML) |
| `inStock` | boolean | Whether the product is purchasable through Lidl's own webshop. Most countries have no online grocery store at all, so this is `false` for virtually every food item regardless of real shelf stock \u2014 treat it as "buyable online," not "in stock in the store." Null only if Lidl's stock data is missing entirely |
| `productId` | string | Lidl internal product ID |
| `country` | string | Country code this product was scraped from |
| `url` | string | Full URL to the product page |
| `scrapedAt` | string | ISO 8601 timestamp |

### Denmark / everyday prices

Lidl Denmark's search API is not a full everyday-shelf price list. Confirmed
by pulling the empty-query catalog: Lidl DK exposes **325 products in
total**, of which 317 sit in the "Mad og drikke" (food & drink) category \u2014
almost the whole online catalog is food, but it's the current **weekly
offer rotation**, not the full range you'd see on a Danish Lidl shelf.
Each item carries a `IN_STORE_FROM_FUTURE_DATE_RANGE` /
`IN_STORE_PAST_DATE_RANGE` badge and an `offerValidFrom`/`offerValidTo`
window \u2014 items whose window hasn't started yet, or has ended, come back
with `price: null` on purpose. Searching a staple like "m\u00e6lk" (milk),
"sm\u00f8r" (butter), "rugbr\u00f8d" (rye bread) or "\u00e6g" (eggs) returns only the
handful of SKUs currently on offer (5-10 items), not every milk/butter/
bread/egg product Lidl DK stocks day to day.

What this means in practice: `unitPrice` (per-litre/per-kg pricing) and
`ean` (barcode) are both present and accurate for Denmark once an item is
in the current offer rotation \u2014 that part is a full, structured feed. What
Lidl DK does not publish anywhere on its site is a static, always-on price
list for its everyday range outside the offer rotation. No scraper can
recover data the retailer doesn't publish.

### Cost estimate

This actor is billed $0.02 per run plus $0.002 per result — **1,000 results cost about $2.02**. Usage cost only settles once a run reports **SUCCEEDED** — reading a dataset mid-run will show a cost far below the real total.

### Tips

- **Weekly offers**: Use `sort: percentageDiscount-desc` to find the biggest current discounts
- **New arrivals**: Use `sort: firstOnlineDate-desc` to see what's new
- **Price comparison**: Run the same search across multiple countries to compare prices
- **Large exports**: Set `maxResults: 10000` to export full product catalogs
- **Datacenter proxies** are usually sufficient — Lidl's API is not heavily protected

### How to scrape Lidl data

1. Create an Apify account (free tier available)
2. Open the Lidl Scraper actor page
3. Enter a search query (e.g. "koffie") and select your target country
4. Click "Start" to begin scraping
5. Download results as JSON, CSV, or Excel when the run finishes

You can also call the actor via the Apify API or schedule recurring runs for automated price monitoring.

### Integrations

Connect Lidl Scraper results to your existing tools and workflows:

- **Google Sheets** — Automatic export after every run
- **Slack / Email** — Get notified when new deals appear or prices drop
- **Webhooks** — Push data to your own API endpoint
- **Zapier / Make** — Connect to 5,000+ apps without code

### Changelog

- **v0.2.5** (2026-09-29) — Lidl Plus app deals now have a price: `price` used to be null on them because Lidl publishes their amount only in the Lidl Plus block. New fields `priceType`, `lidlPlusPrice`, `lidlPlusText`, and `queryInTitle` to tell Lidl's own off-topic search hits apart. All previous fields and input shapes unchanged.
- **v0.2.3** (2026-09-24) — A Lidl product number as `searchQuery` now returns that product (Lidl redirects those searches to the product page, and the run used to fail). A search Lidl answers with a brochure page or with nothing now fails with a message saying so, instead of blaming blocking. Greece now uses lidl-hellas.gr directly. `categoryUrl` now works: it used to be ignored and every run returned the unfiltered catalogue. Category pages (`/h<number>`) are filtered through Lidl's search, offers pages (`/a<number>`) are read from the page. An expired offers page is replaced by the shop's current offers page where the shop links one (BE, NL), and the run says so.
- **v0.2** (2026-09-09) — Added `unitPrice`, `packaging`, `ean`, `offerValidFrom`/`offerValidTo` fields (were present in Lidl's API but not parsed before). `price` is now `null` instead of `0` when Lidl hasn't published one; `inStock` is now nullable instead of defaulting to `false` when stock data is missing. All previous fields and input shapes unchanged.
- **v0.1** (2026-04-07) — Initial release with search API support for 30 countries

# Actor input Schema

## `searchQuery` (type: `string`):

Search term (e.g. 'chocolade', 'laptop', 'grill'), a Lidl product number (e.g. '10030861') or an EAN. Leave empty to browse all products.

## `country` (type: `string`):

Which Lidl country store to scrape.

## `categoryUrl` (type: `string`):

URL of a Lidl category page (ending in /h<number>, e.g. 'https://www.lidl.be/h/nl-BE/badkamer/h10067553') or offers page (ending in /a<number>, e.g. 'https://www.lidl.be/c/nl-BE/acties-deze-week/a10082242'). Overrides the search query. The shop in the URL decides the country. An expired offers page is replaced by the shop's current offers page where the shop links one.

## `sort` (type: `string`):

How to sort results.

## `maxResults` (type: `integer`):

Maximum number of products to scrape. Lidl returns up to 48 per page.

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

Proxy settings. Datacenter proxies usually work fine for Lidl.

## Actor input object example

```json
{
  "searchQuery": "chocolade",
  "country": "BE",
  "sort": "relevancy",
  "maxResults": 100,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# 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 = {
    "searchQuery": "chocolade",
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("studio-amba/lidl-scraper").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 = {
    "searchQuery": "chocolade",
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("studio-amba/lidl-scraper").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 '{
  "searchQuery": "chocolade",
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call studio-amba/lidl-scraper --silent --output-dataset

```

## MCP server setup

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

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/b5FdqIrOmivzpVomk/builds/1zijsSo3OGsSKSDsI/openapi.json
