# Lidl.es Products, Weekly Offers, Prices & Reviews Scraper (`abotapi/lidl-es-scraper`) Actor

Scrape Lidl.es grocery and non-food products across weekly offers, tools, home, garden, kitchen, fashion, kids, sport and more. Search by keyword, department or URL. Extract prices, was-prices, discounts, package formats, images, ratings and full shopper reviews.

- **URL**: https://apify.com/abotapi/lidl-es-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** E-commerce, Automation, Developer tools
- **Features:** Limited permissions
- **Stats:** 8 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 product results

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

## Lidl.es Scraper: Prices, Weekly Offers & Shopper Reviews

Lidl.es Scraper turns Lidl Spain's storefront into structured product data, spanning both the grocery/supermarket weekly-offer range and the "bazaar" (non-food) catalogue: tools, home & garden, kitchen, fashion, baby & kids, and sport & leisure. Search by keyword and department with brand, price, rating and weekly-offer filters, or paste product, search, or department links directly. Every record carries the current price, a genuine strike-through was-price and discount % only when a product is genuinely on offer, the package format, images, and the site's own shopper rating; turn on review fetching for as many shopper reviews as **Max reviews per product** allows (default 20, up to 100; highest-rated first) plus the full rating summary and histogram. Export to JSON, CSV or Excel, or pull results through the API.

### Why This Scraper?

- **Covers both catalogues.** Lidl.es sells grocery weekly offers (fresh produce, pantry, drinks) and a much larger bazaar/non-food range (tools, home, garden, fashion, toys, sport) through the same storefront; this scraper reaches both from one input.
- **Was-price captured only when it's real.** This scraper reads the site's own strike-through price object per item and only ever sets a was-price, discount amount and discount % when the site's own prior price is genuinely populated and higher than the current price. Full-price items get `null`, never a fabricated markdown.
- **Real shopper reviews from the site itself.** Ratings and reviews come from Lidl's own on-site review feed (average, histogram, the **Max reviews per product** highest-rated reviews (default 20, up to 100) with author and votes), included on request or automatically for a pasted product URL.
- **Package format plus a derived unit price.** The package size/format Lidl itself displays (e.g. "750g", or a loose/bulk format) is included whenever Lidl shows it; a EUR/kg or EUR/L unit price is derived only when the package is an unambiguous single weight or volume, never guessed for a bulk or multi-pack item.
- **Two ways in.** Keyword and department browsing with brand, price, rating and weekly-offer filters and sort, or paste any product, search/specials, or department link.
- **Optional export to your apps.** Send results into Notion, Linear, Airtable, or any Apify MCP connector alongside the dataset.

### Use Cases

- **Price and discount tracking:** monitor Lidl's weekly-offer feed for genuine markdowns (was-price, discount %) across any department, on a recurring schedule.
- **Bazaar and non-food sourcing:** pull the tools, home & garden, kitchen, fashion or baby & kids ranges by department for comparison shopping or catalog building.
- **Grocery price monitoring:** track the Alimentacion (grocery) department's current prices and weekly specials for a price-comparison tool.
- **Brand and assortment research:** collect every product under one brand across the whole catalogue to track SKU count, pricing and availability.
- **Shopper sentiment analysis:** pull the site's rating summary, histogram and top reviews for specific products to track shopper sentiment over time.

### Data You Get

> Sample shape: values are illustrative placeholders, not from a live product.

| Field | Example |
| --- | --- |
| `productId` / `title` | `"900000001"`, `"Taladro percutor inalambrico 20V (ejemplo)"` |
| `brand` | `"SAMPLEBRAND"` |
| `categoryPath` / `category` | `["Bricolaje y jardin", "Herramientas", "Herramientas electricas"]`, `"Herramientas electricas"` |
| `productUrl` | `"https://www.lidl.es/p/producto-ejemplo/p900000001"` |
| `price` / `currency` | `39.99`, `"EUR"` |
| `wasPrice` / `discountAmount` / `discountPercent` | `49.99`, `10.0`, `20.0` (only when genuinely on offer) |
| `isOnSpecial` / `promoLabel` | `true`, `"Megadescuento"` |
| `packageFormat` | `"1 ud."` (the site's own package/format text) |
| `unitPrice` / `unitOfMeasure` | `null`, `null` (only set for an unambiguous single weight/volume like `"750g"`) |
| `hasVat` / `onlineAvailable` / `inStoreOnly` / `stockAvailability` | `true`, `true`, `false`, `"En stock"` |
| `images` / `internalArticleNumbers` | full-resolution image URL list, `["555123"]` |
| `description` / `badges` | short plain-text product description as shown on the site, `["Top rated"]` |
| `averageRating` / `reviewCount` / `recommendedYes` / `recommendedNo` | `4.6`, `823`, `780`, `15` |
| `name` / `erpNumber` / `itemNumber` / `productKey` | `"Taladro percutor inalambrico 20V (ejemplo)"`, `"900000001"`, `"555123"`, `"ES:900000001"` |
| `country` / `source` / `scrapedAt` / `categoryLvl0` / `labels` | `"ES"`, `"lidl.es"`, scan timestamp (UTC), top-level department, the site's badge texts |
| `priceEur` / `priceNormal` / `comparePrice` / `compareUnit` | same as `price`; pre-markdown price when on offer, else `price`; same as `unitPrice` / `unitOfMeasure` |
| `packaging` / `unitSize` / `unitMeasure` | `"750g"` as shown, `750.0`, `"g"` (size and unit only for a plain single weight or volume) |
| `availableFrom` / `availableUntil` | offer window (UTC) when Lidl publishes one, else `null` |
| `isOnOffer` / `discountText` / `inStock` | `true`, `"Megadescuento"`, `true` (orderable online) |
| `imageUrl` / `portalUrl` | main image, product page link |
| `reviews` | empty shape by default; full feed only with `fetchDetails`, or automatically for a pasted product URL |

`reviews` always carries the same six keys: `averageRating`, `totalReviewCount`, `recommendedYes`, `recommendedNo`, `starHistogram` and `items` (each with `stars`, `title`, `text`, `author`, `recommended`, `votesUp`, `votesDown`). Without enrichment it is `averageRating: null`, `totalReviewCount: 0`, `items: []`. The extra fields repeat or reshape data already on the card, are not part of incremental change detection (so they never cause an `UPDATED` row by themselves), and the actor covers the Spanish store only. Package price (`price`) comes straight from Lidl's own product data; `unitPrice`/`unitOfMeasure` are never derived for a loose/bulk item ("A granel") or a multi-pack, where dividing by the printed pack size would misstate the true per-unit price.

### How to Use

1. Pick a **mode**: `search` (keyword + department browse with filters) or `url` (paste product, search/specials, or department links).
2. In search mode, set your keyword and/or department, then narrow with brand, price, rating or weekly-offer filters and pick a sort order.
3. Turn on **Fetch shopper reviews** if you want up to **Max reviews per product** shopper reviews (default 20, up to 100) plus the rating summary on every returned product, and set **Max products total** / **Max search pages** to control run size and cost.
4. Click **Start**, then download the dataset as JSON, CSV or Excel, or read it through the API.

**Keyword + department search (default mode):**

```json
{
  "mode": "search",
  "searchTerm": "taladro",
  "category": "bricolaje-jardin",
  "maxItems": 20
}
```

**Weekly offers only, biggest discount first:**

```json
{
  "mode": "search",
  "specialsOnly": true,
  "sortBy": "BIGGEST_DISCOUNT",
  "maxItems": 50
}
```

**One brand within a department, with the full shopper review feed:**

```json
{
  "mode": "search",
  "category": "bricolaje-jardin",
  "brand": "PARKSIDE",
  "fetchDetails": true,
  "maxItems": 20
}
```

**Paste product, search/specials, or department links directly:**

```json
{
  "mode": "url",
  "urls": [
    "https://www.lidl.es/p/producto-ejemplo/p900000001",
    "https://www.lidl.es/c/bricolaje-y-jardin/s99999999",
    "https://www.lidl.es/q/query/bateria"
  ],
  "maxItems": 50
}
```

**Leaving `urls` untouched in URL mode now scrapes one example of each distinct link target** (the field's default is that sample list) instead of nothing. The alternate search form returns the same products as the search link, so it is not in the default. Replace it with your own links to scrape only those.

| Link type | Example shape |
|---|---|
| Product page | `lidl.es/p/<slug>/p<id>`, also the short or slug-less `lidl.es/p/p<id>`, the bare `lidl.es` host, with or without tracking parameters |
| Keyword search | `lidl.es/q/query/<term>` or `lidl.es/q/search?q=<term>` |
| Search with filters | `lidl.es/q/query/<term>?category=<department name>&brand=<brand>&offset=<n>`; every filter (and the start offset) is taken from the link and wins over the run's filter fields |
| Specials | `lidl.es/q/query/descuentos?discountFlag=1` |
| Department page | `lidl.es/h/<slug>/h<id>`, optionally `?offset=48` |
| Department hub | `lidl.es/c/<slug>/s<id>` (the site may redirect a hub to a page with no products; that link then logs a warning and is skipped) |
| Campaign page | `lidl.es/c/<slug>/a<id>` |

A search link whose category or brand matches nothing logs a clear warning instead of returning silently. Category must be the exact department name.

A pasted product URL always gets up to **Max reviews per product** shopper reviews (default 20, up to 100) plus the rating summary, and is still billed one detail-enrichment per product, regardless of the **Fetch shopper reviews** toggle; the user explicitly asked for that one product.

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/lidl-es-scraper").call(run_input={"mode": "search", "searchTerm": "taladro", "maxItems": 20})
for product in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(product["title"], product["price"], product["currency"])
```

JavaScript:

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/lidl-es-scraper').call({ mode: 'search', searchTerm: 'taladro', maxItems: 20 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the **Integrations** tab.

#### How maxItems and maxPages interact

Search mode pages through results in batches, stopping once either **Max products total** or **Max search pages** is reached, whichever comes first. Leave **Max search pages** empty for no page limit (bounded only by **Max products total**). URL mode gives each pasted URL a fair share of what is left of **Max products total**; a URL that needs fewer rows (for example a single product link) passes its unused share on to the later URLs, and every URL gets at least one row.

#### Output views

The dataset has four views in the Console: **Products** (core price, offer and rating columns), **Reviews (nested)** (one row per product with its rating summary and review list nested, when reviews were fetched), **Changes (incremental)** (change type, changed fields, first/last seen, previous price) and **Details & availability** (package format, offer window, stock and extra attributes). All views read the same records.

### Send results into your apps (MCP connectors)

Optionally pipe results into the apps you already use through Model Context Protocol (MCP) connectors. Authorize a connector once under Apify, Settings, Integrations, then select it in the `mcpConnectors` field. Each connector receives a condensed, human-readable summary per product (title plus key fields), while the complete record always stays in the Apify dataset. For Notion, set `notionParentPageUrl` to the page the item pages should be created under, and use `maxNotifyListings` to cap how many items are exported per connector per run. Supported connectors: Notion, Linear, Airtable, and Apify. Leave the field empty to skip; it never changes the dataset output.

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `search` | `search` browses by keyword/department filters; `url` scrapes pasted product, search/specials, or department links. |
| `searchTerm` | string | no default (prefill: `bateria`) | Free-text keyword against Lidl's own search index (search mode only). Leave empty with a department set to browse the whole department. |
| `category` | string | `""` (Any department) | One of Lidl's real top-level departments, spanning both the bazaar/non-food and grocery ranges (search mode only). |
| `brand` | string | no default | Exact brand name to keep, as Lidl shows it, e.g. `PARKSIDE` (search mode only). |
| `specialsOnly` | boolean | `false` | Keep only products on a genuine current weekly offer (search mode only). |
| `inStockOnly` | boolean | `false` | Drop products that cannot be ordered online right now; Max products counts only the rows kept. |
| `strictMatch` | boolean | `false` | Search mode: keep a product only when every word of the keyword appears in its title, brand or category. |
| `minPrice` | number | no default | Minimum price in EUR to keep (search mode only). |
| `maxPrice` | number | no default | Maximum price in EUR to keep (search mode only). |
| `minRating` | number | no default | Minimum shopper rating (1-5) to keep (search mode only). |
| `sortBy` | string | `RELEVANCE` | `RELEVANCE`, `PRICE_ASC`, `PRICE_DESC`, `NEWEST`, `TOP_RATED`, `BESTSELLERS`, or `BIGGEST_DISCOUNT` (search mode only). |
| `urls` | array | one sample link per supported link type | Product, search/specials, or department listing URLs to scrape (url mode only); filter fields above are ignored in this mode. |
| `maxReviewsPerProduct` | integer | `20` | Shopper reviews returned per product when reviews are fetched (1 to 100, highest-rated first). Does not change the per-product review charge. |
| `fetchDetails` | boolean | `false` | Fetch up to `maxReviewsPerProduct` (default 20) shopper reviews plus the rating summary and star histogram for each product. Always on, regardless of this toggle, for a pasted product URL. |
| `maxPages` | integer | no default (empty = unlimited) | Stop after fetching this many search result pages. |
| `maxItems` | integer | `20` | Hard cap on total products returned across the run (up to 1,000,000; `0` is read as that maximum). |
| `resumeFromRunId` | string | empty | ID of an earlier run (or dataset) of this actor. Products already in it are skipped and not charged, so the run returns only new products. A resumed run never marks products as expired. |
| `proxy` | object | `{"useApifyProxy": true}` | Apify Proxy configuration used to route requests. The default works out of the box. |
| `incrementalMode` | boolean | `false` | Recurring monitoring. The first run returns every match as `NEW`; later runs return only `NEW`, `UPDATED` and `REAPPEARED` products, each with `changeType`, `changedFields`, `firstSeenAt`, `lastSeenAt` (and `previousPrice` on a price move). Compared: price, was price, discount, offer flag, online availability, stock status, title, brand, category, package format, description, badges and link. Never compared (kept in the row): shopper rating, review count, review feed, image list. Max products counts returned rows only, so a recurring run walks the whole search. |
| `stateKey` | string | no default | Names a monitoring campaign so its saved state stays stable. Leave empty to derive it from your search, URLs and filters. |
| `emitUnchanged` | boolean | `false` | Also return products with no change, marked `UNCHANGED`. Each returned row is billed like any other product. |
| `emitExpired` | boolean | `false` | Also return products no longer listed, marked `EXPIRED`, only after a complete scan (never after a Max products / Max search pages stop, a pasted department listing page, or a state key reused with different filters). A listing drop and a delisting look the same: read `onlineAvailable` and `stockAvailability` for availability. |
| `changeScope` | string | `all` | `priceAndStock` returns an `UPDATED` row only when price, was price, discount, offer flag, online availability or stock status moves. Changing it without a `stateKey` starts a new automatic baseline. |
| `compactUpdates` | boolean | `false` | `UPDATED` and `EXPIRED` rows are short (id, title, link, price, availability, `priceChanges` old/new). `NEW` rows stay complete. Shopper reviews are not fetched or billed for compact rows. |
| `expireAfterMissedScans` | integer | `1` | With `emitExpired`: expire a product only after it was missing from this many complete scans in a row. |
| `ignoreFieldsForChanges` | array | no default | Incremental mode only: extra field names (or dotted paths) that never make a product `UPDATED`. |
| `mcpConnectors` | array | no default | Optional MCP connectors to export results into (Notion, Linear, Airtable, Apify). |
| `notionParentPageUrl` | string | no default | Notion connector only: page under which item pages are created. |
| `maxNotifyListings` | integer | `50` | Cap on items exported to each connector per run. Does not affect the dataset. |

### Output Example

> Sample shape: values are illustrative placeholders, not from a live product.

```json
{
  "productId": "900000001",
  "title": "Taladro percutor inalambrico 20V (ejemplo)",
  "brand": "SAMPLEBRAND",
  "categoryPath": ["Bricolaje y jardin", "Herramientas", "Herramientas electricas"],
  "category": "Herramientas electricas",
  "productUrl": "https://www.lidl.es/p/producto-ejemplo/p900000001",
  "price": 39.99,
  "currency": "EUR",
  "wasPrice": 49.99,
  "discountAmount": 10.0,
  "discountPercent": 20.0,
  "isOnSpecial": true,
  "promoLabel": "Megadescuento",
  "packageFormat": "1 ud.",
  "hasVat": true,
  "unitPrice": null,
  "unitOfMeasure": null,
  "onlineAvailable": true,
  "inStoreOnly": false,
  "stockAvailability": "En stock",
  "images": ["https://www.lidl.es/assets/sample-image.jpg"],
  "internalArticleNumbers": ["555123"],
  "description": "Descripcion corta de ejemplo del producto, tal como aparece en el sitio.",
  "badges": ["Top rated"],
  "averageRating": 4.6,
  "reviewCount": 823,
  "recommendedYes": 780,
  "recommendedNo": 15,
  "name": "Taladro percutor inalambrico 20V (ejemplo)",
  "erpNumber": "900000001",
  "itemNumber": "555123",
  "productKey": "ES:900000001",
  "country": "ES",
  "categoryLvl0": "Bricolaje y jardin",
  "priceEur": 39.99,
  "priceNormal": 49.99,
  "packaging": "1 ud.",
  "isOnOffer": true,
  "discountText": "Megadescuento",
  "inStock": true,
  "source": "lidl.es",
  "scrapedAt": "2026-01-01T00:00:00Z",
  "reviews": {
    "averageRating": 4.6,
    "totalReviewCount": 823,
    "recommendedYes": 780,
    "recommendedNo": 15,
    "starHistogram": { "1": 8, "2": 10, "3": 30, "4": 150, "5": 625 },
    "items": [
      { "stars": 5, "title": "Buena relacion calidad-precio", "text": "Texto de resena de ejemplo.", "author": "Comprador de ejemplo", "recommended": true, "votesUp": 4, "votesDown": 0 }
    ]
  }
}
```

### Plan Requirement

The default Apify Proxy setting (datacenter) works for this site: it was measured on product, search, specials, department and campaign pages and returned the same results as the residential group, so no special plan or proxy group is needed. Residential is optional and only adds headroom for very large or very frequent runs. If your Apify plan does not allow proxy access at all, or a run cannot load any page, the run ends as failed with a plain message telling you so (see the FAQ). The **Pricing** tab shows the current rates for your plan.

### FAQ

#### How much does it cost?

Billing is pay per event, and the **Pricing** tab lists the exact rates for your plan. Three events exist: a one-time charge when the run starts; one **Product result** event per product returned in the dataset; and one **detail-enrichment** event per product whose full shopper review feed was fetched and delivered (once per product, whatever the review count or the `maxReviewsPerProduct` value; via **Fetch shopper reviews**, or automatically for a pasted product URL). A failed review fetch, a compact incremental row and a suppressed unchanged row are not charged for enrichment. The enrichment event is charged right after each enriched product is delivered, so an interrupted run is billed only for what it returned. Use **Max products total** to cap the cost of any run.

#### Is it legal to scrape Lidl.es?

This actor collects only publicly available product data (prices, offers, descriptions, ratings and reviews) as shown on Lidl.es. You are responsible for how you use it: follow Lidl's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Product images and review text may be subject to third-party rights.

#### Can I schedule it to catch new weekly offers?

Yes, schedule the actor from the **Schedules** tab, e.g. with `specialsOnly: true` and a department set. Turn on `incrementalMode` to receive only what changed since the previous scheduled run (new products, price or availability moves); without it every run returns the current matching products in full. Billing: with incremental mode on, only the rows returned are billed (`UNCHANGED` rows only if you ask for them), and shopper reviews are fetched only for returned, non-compact rows.

#### Why are wasPrice and unitPrice sometimes null?

Both are reported only when genuinely applicable. `wasPrice`/`discountAmount`/`discountPercent`/`isOnSpecial` are set only when Lidl's own strike-through price field is genuinely populated and higher than the current price; most full-price items simply carry no was-price. `unitPrice`/`unitOfMeasure` are derived only when the package format is an unambiguous single weight or volume (e.g. `"750g"`); a loose/bulk item or a multi-pack always gets `null` rather than a guessed figure.

#### What happens when a run cannot load Lidl pages?

A run that could not load a single page does not finish as an empty success: it ends as failed with a plain message and nothing is billed for products. The messages are: "The proxy connection was refused for this account, so no page could be read. Your Apify plan does not include proxy access; a paid plan is required" (account without proxy access, or invalid custom proxy URLs); "No results: residential proxies are unavailable on this run (Apify free plans do not include them), and the backup connection could not load the requested Lidl pages" (you chose residential on a plan without it); and "Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search. Please try again later." When pages loaded but your filters matched nothing, the run succeeds with the status "No products matched the given filters." and the log says "No products matched. Nothing was pushed for this run." A partial failure mid-run keeps the products already collected. If the chosen connection is unavailable the run continues on a backup connection and logs that it did so.

#### Can I use it with AI agents or MCP?

Yes. Call it from any Apify integration or MCP client, and use the connector field to push results into Notion, Linear or Airtable.

### 🔗 Want more grocery data?

Pair this actor with these related scrapers from the same team:

<table>
<tr><td>🥦 <a href="https://apify.com/abotapi/mercadona-es-scraper"><b>Mercadona Scraper</b></a><br>Scrape Mercadona products by delivery-area postal code. Search by keyword or category, or...</td><td>🥦 <a href="https://apify.com/abotapi/elcorteingles-es-scraper"><b>El Corte Inglés Scraper</b></a><br>Scrape El Corte Inglés elcorteingles.es products across fashion, electronics, home...</td></tr>
<tr><td>🥦 <a href="https://apify.com/abotapi/carrefour-es-scraper"><b>Carrefour Spain Scraper</b></a><br>Scrape Carrefour Spain (carrefour.es) products. Search by keyword, browse a category, or...</td><td>🥦 <a href="https://apify.com/abotapi/auchan-fr-scraper"><b>Auchan France Scraper</b></a><br>Scrape Auchan.fr products: grocery, drinks, household and general merchandise. Real price...</td></tr>
<tr><td>🥦 <a href="https://apify.com/abotapi/foodhero-surplus-grocery-scraper"><b>FoodHero Scraper</b></a><br>Scrape FoodHero surplus grocery deals across Canada by area, store or offer ID. Extract...</td><td>🥦 <a href="https://apify.com/abotapi/resq-club-surplus-food-scraper"><b>ResQ Club Scraper</b></a><br>Scrape ResQ Club surplus food offers in Finland, Sweden and Estonia: name, price, current...</td></tr>
</table>

👉 [Browse all abotapi scrapers](https://apify.com/abotapi)

### 💬 Support

- 🐞 **Found a bug or a missing field?** Open a ticket on the [Issues tab](https://apify.com/abotapi/lidl-es-scraper/issues/open). We usually reply within hours.
- 🛠️ **Need another site, extra fields or a private build?** Email <contact@abotapi.com>.
- ⭐ **Enjoying it?** A quick review on the actor page helps other users find it.

# Actor input Schema

## `mode` (type: `string`):

'search' browses the Lidl.es catalogue (grocery weekly offers + the bazaar/non-food range) by keyword, department, brand and filters. 'url' scrapes any product, search/specials, or category link you paste.

## `searchTerm` (type: `string`):

Free-text keyword against Lidl's own search index, e.g. 'bateria' or 'sandia'. Leave empty (with a Department set) to browse a whole department.

## `category` (type: `string`):

Narrow to one of Lidl.es's real top-level departments, spanning both the bazaar/non-food weekly range and grocery. Leave as 'Any' to search across the whole catalogue.

## `brand` (type: `string`):

Only return products from this brand, exactly as Lidl shows it (e.g. 'PARKSIDE', 'SILVERCREST', 'CRIVIT'). Leave empty for all brands.

## `specialsOnly` (type: `boolean`):

Narrows to Lidl's own genuine weekly-offer markdowns (real strike-through was-price + discount %) -- the same set as the site's own 'Descuentos' page.

## `inStockOnly` (type: `boolean`):

Drop products that are not currently orderable online (sold out or in-store only). Applied to the rows returned, so Max products counts only in-stock rows.

## `strictMatch` (type: `boolean`):

Search mode only: keep a product only when every word of the search keyword appears in its title, brand or category, dropping loose relevance matches from Lidl's own search.

## `minPrice` (type: `number`):

Only return products priced at or above this amount, in euros.

## `maxPrice` (type: `number`):

Only return products priced at or below this amount, in euros.

## `minRating` (type: `number`):

Only return products with a shopper rating at or above this value.

## `sortBy` (type: `string`):

Sort order for the results, using Lidl's own real sort options.

## `urls` (type: `array`):

Paste one or more Lidl.es links. Supported: product pages (lidl.es/p/<slug>/p<id>, also the short or slug-less lidl.es/p/p<id> form and the bare lidl.es host), search and specials pages (lidl.es/q/query/<term> or lidl.es/q/search?q=<term>, with category, brand, discountFlag, sort, price, ratingAverage and offset taken from the link), department pages (lidl.es/h/<slug>/h<id>, optionally ?offset=48), department hubs (lidl.es/c/<slug>/s<id>) and campaign pages (lidl.es/c/<slug>/a<id>). Leaving this field untouched scrapes one example of each distinct link target (the alternate search form returns the same products as the search link, so it is listed in the examples but not in the default). Filter fields above are ignored in this mode.

## `maxPages` (type: `integer`):

Stop after fetching this many result pages. Leave empty for no limit -- the run stops at Max products total.

## `maxItems` (type: `integer`):

Hard cap on total products returned across the run (maximum 1,000,000; 0 is read as that maximum). Also bounded by Max search pages.

## `fetchDetails` (type: `boolean`):

Adds the site's own full review feed (rating breakdown, star histogram, per-review text/author/votes) for each product. Current price, was-price, unit price, images, description and the rating summary are already included without this toggle. Always on for a pasted product URL.

## `maxReviewsPerProduct` (type: `integer`):

How many shopper reviews to return per product when reviews are fetched (highest-rated first). Between 1 and 100.

## `proxy` (type: `object`):

Apify Proxy configuration used to route requests. The default works out of the box.

## `resumeFromRunId` (type: `string`):

Optional. ID of a previous run of this actor (or a dataset ID). Products already in that dataset are skipped, so this run returns only NEW products (a delta); skipped products are not charged. Combine both runs' datasets for the full set. Max products then counts only the new products. A resumed run never marks products as expired. For recurring monitoring of the same search, use Incremental mode below instead.

## `incrementalMode` (type: `boolean`):

Turn this on for daily or recurring monitoring. The first run returns all matching products as NEW. Later runs compare every product against the last run (price, was price and discount, offer flag, online availability and stock status, title, brand, category, package format, description, badges and product link) and return only NEW, UPDATED and REAPPEARED products, with an UPDATED row's changedFields naming exactly what moved. Shopper ratings, review counts, the review feed and the image list are never compared, since those change on their own with no real product change; they stay in the rows you receive. Turn on "Emit unchanged" or "Emit expired" only when you also want those products returned (and billed). Max products counts returned rows only, so a recurring run walks the whole search to find changes. State is kept separately for each search/URL/filter setup; use State key to name or deliberately share a campaign.

## `stateKey` (type: `string`):

Optional. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently-configured runs. Leave empty to let the actor derive a key automatically from the search/URL and filter settings; different setups then never mix state with each other.

## `emitUnchanged` (type: `boolean`):

Off by default. Turn on to also return products that have not changed since the last run, marked UNCHANGED. This returns and bills extra rows you already have, so leave it off unless you specifically want the full snapshot every run.

## `emitExpired` (type: `boolean`):

Off by default. Turn on to also return products that were present in a previous run but are no longer listed, marked EXPIRED. Only produced after a complete scan of the tracked search or URLs: never when Max products or Max search pages stopped it early, for a pasted department listing page, or when the state key was last used with different filters. EXPIRED means the product dropped out of the listing; without a detail check a listing drop and a delisting look the same, so read stock status from onlineAvailable and stockAvailability. This returns and bills extra rows, so leave it off unless you need expiry tracking.

## `changeScope` (type: `string`):

All fields (default): any change to the product record, except the always-ignored rating, review and image fields, returns an UPDATED row. Price and availability only: only a change to price, was price, discount, offer flag, online availability or stock status returns an UPDATED row; descriptions, titles, badges and other details never do. Changing this on a search with no State key starts a new automatic baseline (first run returns every product as NEW), so set a State key to keep your baseline.

## `compactUpdates` (type: `boolean`):

Off by default. When on, UPDATED (and EXPIRED) rows are short: product id, title, link, current price and availability, plus priceChanges showing the old and new value of each moved price or availability field. NEW rows always carry the complete product record. Shopper reviews are not included in compact rows and are not billed for them.

## `expireAfterMissedScans` (type: `integer`):

Only used with Emit expired products. A product is marked EXPIRED only after it has been missing from this many complete scans in a row (default 1). Raise it (e.g. 3) so a product that briefly drops out of the listing is not reported as gone. A product that comes back resets the count.

## `ignoreFieldsForChanges` (type: `array`):

Optional. Field names to exclude from UPDATED change detection, in addition to the built-in handling (ratings, review counts, the review feed and the image list never trigger an UPDATED). Use this if another field in your setup turns out to be noisy, e.g. promoLabel or badges. Changing this on a search with no State key starts a new automatic baseline.

## `mcpConnectors` (type: `array`):

Optionally send the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize a connector once under Apify -> Settings -> Integrations, then select it here. The connector receives a condensed, human-readable summary per item (title + key fields), not the full JSON; the complete record stays in the dataset. Leave empty to skip. Supported: Notion (https://mcp.notion.com/mcp), Linear (https://mcp.linear.app/sse), Airtable (https://mcp.airtable.com/mcp), Apify (https://mcp.apify.com).

## `notionParentPageUrl` (type: `string`):

URL (or id) of the Notion page under which item pages are created. Required to enable the Notion export; ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on items written to each connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "search",
  "searchTerm": "bateria",
  "category": "",
  "specialsOnly": false,
  "inStockOnly": false,
  "strictMatch": false,
  "sortBy": "RELEVANCE",
  "urls": [
    "https://www.lidl.es/p/parkside-bateria-de-20-v-4-ah-pap-204-a1/p100398189",
    "https://www.lidl.es/p/parkside-bateria-4-ah-20-v/p100398189",
    "https://www.lidl.es/p/p100398189",
    "https://lidl.es/p/parkside-bateria-de-20-v-4-ah-pap-204-a1/p100398189?utm_source=newsletter",
    "https://www.lidl.es/q/query/bateria",
    "https://www.lidl.es/q/query/bateria?category=Bricolaje%20y%20jard%C3%ADn&brand=PARKSIDE&offset=36",
    "https://www.lidl.es/q/query/descuentos?discountFlag=1",
    "https://www.lidl.es/q/search?q=bateria",
    "https://www.lidl.es/h/aspiradoras/h10084470",
    "https://www.lidl.es/h/aspiradoras/h10084470?offset=48",
    "https://www.lidl.es/c/bricolaje-y-jardin/s10067761",
    "https://www.lidl.es/c/aprender-jugando-vale-la-pena/a10106306"
  ],
  "maxItems": 20,
  "fetchDetails": false,
  "maxReviewsPerProduct": 20,
  "proxy": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "changeScope": "all",
  "compactUpdates": false,
  "expireAfterMissedScans": 1,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `reviews` (type: `string`):

No description

## `changes` (type: `string`):

No description

## `details` (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 = {
    "mode": "search",
    "searchTerm": "bateria",
    "urls": [
        "https://www.lidl.es/p/parkside-bateria-de-20-v-4-ah-pap-204-a1/p100398189",
        "https://www.lidl.es/p/parkside-bateria-4-ah-20-v/p100398189",
        "https://www.lidl.es/p/p100398189",
        "https://lidl.es/p/parkside-bateria-de-20-v-4-ah-pap-204-a1/p100398189?utm_source=newsletter",
        "https://www.lidl.es/q/query/bateria",
        "https://www.lidl.es/q/query/bateria?category=Bricolaje%20y%20jard%C3%ADn&brand=PARKSIDE&offset=36",
        "https://www.lidl.es/q/query/descuentos?discountFlag=1",
        "https://www.lidl.es/q/search?q=bateria",
        "https://www.lidl.es/h/aspiradoras/h10084470",
        "https://www.lidl.es/h/aspiradoras/h10084470?offset=48",
        "https://www.lidl.es/c/bricolaje-y-jardin/s10067761",
        "https://www.lidl.es/c/aprender-jugando-vale-la-pena/a10106306"
    ],
    "maxReviewsPerProduct": 20,
    "proxy": {
        "useApifyProxy": true
    },
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "changeScope": "all",
    "compactUpdates": false,
    "expireAfterMissedScans": 1,
    "ignoreFieldsForChanges": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/lidl-es-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 = {
    "mode": "search",
    "searchTerm": "bateria",
    "urls": [
        "https://www.lidl.es/p/parkside-bateria-de-20-v-4-ah-pap-204-a1/p100398189",
        "https://www.lidl.es/p/parkside-bateria-4-ah-20-v/p100398189",
        "https://www.lidl.es/p/p100398189",
        "https://lidl.es/p/parkside-bateria-de-20-v-4-ah-pap-204-a1/p100398189?utm_source=newsletter",
        "https://www.lidl.es/q/query/bateria",
        "https://www.lidl.es/q/query/bateria?category=Bricolaje%20y%20jard%C3%ADn&brand=PARKSIDE&offset=36",
        "https://www.lidl.es/q/query/descuentos?discountFlag=1",
        "https://www.lidl.es/q/search?q=bateria",
        "https://www.lidl.es/h/aspiradoras/h10084470",
        "https://www.lidl.es/h/aspiradoras/h10084470?offset=48",
        "https://www.lidl.es/c/bricolaje-y-jardin/s10067761",
        "https://www.lidl.es/c/aprender-jugando-vale-la-pena/a10106306",
    ],
    "maxReviewsPerProduct": 20,
    "proxy": { "useApifyProxy": True },
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "changeScope": "all",
    "compactUpdates": False,
    "expireAfterMissedScans": 1,
    "ignoreFieldsForChanges": [],
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/lidl-es-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 '{
  "mode": "search",
  "searchTerm": "bateria",
  "urls": [
    "https://www.lidl.es/p/parkside-bateria-de-20-v-4-ah-pap-204-a1/p100398189",
    "https://www.lidl.es/p/parkside-bateria-4-ah-20-v/p100398189",
    "https://www.lidl.es/p/p100398189",
    "https://lidl.es/p/parkside-bateria-de-20-v-4-ah-pap-204-a1/p100398189?utm_source=newsletter",
    "https://www.lidl.es/q/query/bateria",
    "https://www.lidl.es/q/query/bateria?category=Bricolaje%20y%20jard%C3%ADn&brand=PARKSIDE&offset=36",
    "https://www.lidl.es/q/query/descuentos?discountFlag=1",
    "https://www.lidl.es/q/search?q=bateria",
    "https://www.lidl.es/h/aspiradoras/h10084470",
    "https://www.lidl.es/h/aspiradoras/h10084470?offset=48",
    "https://www.lidl.es/c/bricolaje-y-jardin/s10067761",
    "https://www.lidl.es/c/aprender-jugando-vale-la-pena/a10106306"
  ],
  "maxReviewsPerProduct": 20,
  "proxy": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "changeScope": "all",
  "compactUpdates": false,
  "expireAfterMissedScans": 1,
  "ignoreFieldsForChanges": []
}' |
apify call abotapi/lidl-es-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/lidl-es-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/EKomWBjrufCCdvSqb/builds/4k79HqIWGs6kw7P9f/openapi.json
