# LeroyMerlin.es Scraper: Products, Prices, Sellers & Reviews (`abotapi/leroymerlin-es-scraper`) Actor

Scrape Leroy Merlin Spain (leroymerlin.es) DIY and home-improvement products: price, strike-through original price and discount, seller identity (Leroy Merlin vs marketplace) with every competing offer, stock, rating and individual reviews. Search by keyword/category or paste product/listing links.

- **URL**: https://apify.com/abotapi/leroymerlin-es-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** E-commerce, Automation, Developer tools
- **Features:** Limited permissions
- **Stats:** 12 total users, 4 monthly users, 82.4% runs succeeded, 1 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

## Leroy Merlin Spain Scraper: Prices, Discounts, Seller Offers & Reviews

Leroy Merlin Spain Scraper turns leroymerlin.es into a clean product data feed. Get the current price, the strike-through original price and discount when a product is genuinely on promotion, marketplace seller identity with every competing offer, stock and delivery time, rating, and individual customer reviews for tools, appliances and home-improvement products. Search by keyword, a real site category, or paste any product or listing link directly, then export to JSON, CSV or Excel, or pull results straight into your app through the API.

### Why This Scraper?

- **Was-price and discount, structured, not scraped from a badge.** When a product is marked down, both the current and original price come back plus the discount amount and percentage. Full-price products carry `null`, never a fabricated discount.
- **Marketplace transparency.** Leroy Merlin sells both directly and through third-party sellers on the same product page. Every record identifies the seller (Leroy Merlin vs marketplace) and, with detail on, lists every competing offer for that product, price, stock and delivery time per seller.
- **Reviews straight from the product page.** Overall rating, review count, and individual customer reviews (author, date, rating, body) come from the page itself, not a third-party review vendor.
- **Two ways in.** Keyword or category search with the site's own price, rating, promotion and seller filters and sort, or paste any product or listing link and let pagination continue automatically.
- **Filters that actually narrow, not just trim afterward.** Price, rating, promotion and seller filters are checked while pages are walked, so a small item cap still finds enough matching products instead of stopping on an unlucky first page.
- **A sort you can trust.** Price sort is re-applied after detail enrichment on the exact price that ships, so within each search or pasted listing link, row order and price values never disagree.
- **Optional export to your apps.** Send results into Notion, Linear, Airtable, or any Apify MCP connector alongside the dataset.

### Use Cases

- **Price and promotion tracking:** watch was-price, discount percentage and stock across a keyword or category to catch sales as they go live.
- **Competitive repricing:** compare Leroy Merlin's own price against every third-party marketplace offer on the same product page.
- **Marketplace share research:** track seller composition and total offer count to see how many sellers compete for a given product line.
- **Reputation research:** pull rating history and individual review text for products you are sourcing, reviewing or comparing.
- **Catalog audits:** walk a full category to check assortment, stock status and delivery promises.
- **Deal-alert bots:** filter to on-promotion products only and feed the results into your own alerting tool.

### Data You Get

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

| Field | Example |
| --- | --- |
| `productId` / `sku` | `"123456"` |
| `name` | `"Sample Cordless Drill 18V"` |
| `brand` | `"SAMPLE BRAND"` |
| `category` / `categoryPath` | `"Cordless drills"` / `["Tools", "Power tools", "Drilling & screwing", "Drills", "Cordless drills"]` |
| `url` | `"https://www.leroymerlin.es/productos/sample-cordless-drill-18v-123456.html"` |
| `price` / `priceExclTax` / `currency` | `99.99` / `82.64` / `"EUR"` |
| `originalPrice` / `discountAmount` / `discountPercent` | `129.99` / `30.0` / `23.1` |
| `isOnSpecial` | `true` |
| `rating` | `4.7` |
| `sellerId` / `sellerName` / `sellerType` | `"002"` / `"Sample Retailer"` / `"1P"` |
| `totalOfferCount` / `sellersComposition` | `12` / `"1P_3P"` |
| `offerType` | `"NEW"` |
| `sponsored` | `false` |
| `ean` | `"4000000000000"` |
| `description` | full product description text |
| `image` / `numberOfImages` | `"https://media.example.com/sample.jpg"` / `5` |
| `stock` / `stockStatus` / `deliveryTime` | `19` / `"ONSITE"` / `"1 OPENING_DAY"` |
| `specs` | `{"Tensión (en V)": "4", "Batería incluida": "Sí", "Marca del producto": "BOSCH"}` |
| `otherOffers[]` | `[{"sellerName": "Sample Marketplace Seller", "sellerType": "3P", "price": 105.40, "stock": 3, "deliveryTime": "3 OPENING_DAY"}]` |
| `numberOfReviews` / `reviewCount` | `275` / `275` |
| `imageUrl` / `inStock` / `scrapedAt` | same image as `image` / `true` when `stock` is above 0 / ISO-8601 UTC timestamp of the scrape |
| `reviews.averageRating` / `reviews.totalReviewCount` | `4.7` / `275` |
| `reviews.items[]` | `[{"rating": 5, "body": "Sample review text.", "author": "Sample Customer", "date": "2026-07-15"}]` |
| `searchMode` | `"search"` (also `"category"` or `"url"`) |

`specs` is the product page's technical-attribute table as attribute name to text value (units are part of the name or the value, exactly as the site shows them); it is returned when **Fetch product detail** is on and omitted when the page has no table. `otherOffers` lists every competing marketplace offer visible on the product page at fetch time, price, stock and delivery per seller; a product sold exclusively by Leroy Merlin with no marketplace competition simply omits the field rather than returning an empty placeholder. `reviews.items[]` holds the product page's own most recent reviews (the page itself only ever surfaces a handful), while `reviews.averageRating` and `reviews.totalReviewCount` reflect the product's full review history, not just the reviews included in `items[]`.

### How to Use

1. Pick a **mode**: `search` (keyword and/or category, with filters and sort) or `url` (paste product or listing links).
2. For search mode, fill in a keyword and/or a category path, and set any price, rating, promotion or seller filters plus a sort order. For url mode, paste one or more product or listing links.
3. Leave **Fetch product detail** and **Fetch reviews** on for full records, or turn them off for a lighter listing-only run. Set **Max products** and **Max pages** to control run size and cost.
4. Click **Start**. Download the dataset as JSON, CSV or Excel, or read it through the API.

**Keyword search, first page, with detail and reviews (default):**

```json
{
  "mode": "search",
  "searchTerm": "taladro",
  "maxItems": 20
}
```

**Category browse, only products on promotion, sorted by price:**

```json
{
  "mode": "search",
  "category": "herramientas/herramientas-electricas-portatiles/herramientas-electricas-para-taladrar-atornillar/taladros/taladros-de-bateria",
  "onPromotionOnly": true,
  "sortBy": "price_asc",
  "maxItems": 50,
  "maxPages": 5
}
```

**Only Leroy Merlin's own offers, minimum 4-star rating:**

```json
{
  "mode": "search",
  "searchTerm": "grifo cocina",
  "sellerFilter": "leroymerlin",
  "minRating": 4,
  "maxItems": 30
}
```

**Full detail and reviews for a pasted product link:**

```json
{
  "mode": "url",
  "urls": ["https://www.leroymerlin.es/productos/sample-cordless-drill-18v-123456.html"],
  "fetchDetails": true,
  "fetchReviews": true
}
```

**Accepted link shapes (url mode).** Search results (`https://www.leroymerlin.es/search?q=taladro`), category listings (`https://www.leroymerlin.es/productos/<category-path>/`) and product pages (`https://www.leroymerlin.es/productos/<slug>-<id>.html`). Listing links accept `p=<n>` for the start page and `sort=relevance|price-asc|price-desc`. If you leave **Links** untouched, url mode scrapes one example of each shape (the three defaults); clear the field or paste your own links to change that. In search mode a category given by name is converted to its path, and a warning is logged if nothing matches.

**Reviews.** Reviews are on by default with a limit of 20 per product (maximum 1000). The product page itself exposes only its most recent handful, so the practical ceiling is lower than the cap.

**Dataset views.** Besides the default table, the Output tab has an **Other offers** view (one row per competing marketplace offer) and a **Reviews** view (one row per review).

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/leroymerlin-es-scraper").call(run_input={"mode": "search", "searchTerm": "taladro", "maxItems": 20})
for product in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(product["name"], 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/leroymerlin-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 filters narrow the page walk

Price, rating, promotion and seller filters are checked as each result page is read, not only after the whole batch is collected. That means **Max products** means "N products that actually pass your filters", so a run with `onPromotionOnly` on and a small `maxItems` keeps walking pages (up to **Max pages**) until enough promoted products are found, instead of stopping early on a page where none happened to qualify.

#### Price sort after enrichment

With **Fetch product detail** on (the default), each product's price can refresh to the detail page's own value after the listing page was already sorted. The run re-applies your chosen sort on that final price immediately before the dataset is written, so `sortBy` always matches the price values you actually see in the output.

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

Optionally pipe the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step **after** the scrape: the Apify dataset is never changed.

**What gets written to the connector:** a condensed, human-readable **summary** of each product, not the full JSON. Each item becomes one entry with a **title** and its key fields flattened to plain text. The **complete record always stays in the Apify dataset**.

1. Authorize a connector once under **Apify, Settings, Integrations** (Notion, Linear, Airtable, or Apify).
2. Select it in the **"Pipe results into your apps"** input field. If the picker is empty, you haven't authorized a connector yet.
3. For **Notion**, also set `notionParentPageUrl` to the page where item pages should be created.

Leave the field empty to skip. A connector error never fails the run or changes the dataset; it is logged as a warning and the run continues.

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `search` | `search` or `url`. |
| `searchTerm` | string | none (prefill: `taladro`) | Free-text keyword (search mode). Leave empty and use Category only to browse without a keyword. If search mode gets neither a keyword nor a Category, the run searches the example keyword `taladro` instead of returning nothing; it is billed like any run (rows delivered), and with Incremental mode on it neither reads nor saves state. |
| `category` | string | none | A real leroymerlin.es catalog category path (search mode only). Ignored when `searchTerm` is set. |
| `sortBy` | string | `relevance` | `relevance`, `price_asc`, or `price_desc`. Applies to keyword/category search and pasted listing URLs alike. |
| `minPrice` | number | none | Only return products priced at or above this amount, in EUR. |
| `maxPrice` | number | none | Only return products priced at or below this amount, in EUR. |
| `minRating` | number | none | Only return products whose average rating is at least this many stars (0-5). |
| `onPromotionOnly` | boolean | `false` | Keep only products currently showing the site's own strike-through original price and discount. |
| `sellerFilter` | string | `any` | `any`, `leroymerlin`, or `marketplace`. |
| `urls` | array | three example links, one per shape (keyword search, category listing, product page) | Product or listing links to scrape (url mode only). If you leave this field untouched, url mode scrapes those three examples; clear the field or paste your own links to change it. Pagination continues forward automatically from any `?p=` already present. |
| `fetchDetails` | boolean | `true` | Fetch EAN, description, image, breadcrumb category path, stock/delivery, and every competing marketplace offer. |
| `fetchReviews` | boolean | `true` | Fetch the product's own reviews. Shares the same page fetch as `fetchDetails`. |
| `maxReviewsPerProduct` | integer | `20` | Cap on reviews returned per product (maximum 1000). The site embeds only its most recent ~8 reviews per product page, so that is the practical ceiling; the rating and total count reflect the full history. |
| `maxPages` | integer | none (an empty or zero value behaves as an internal cap of 100 pages per search, category, or URL entry) | Stop after this many result pages per search, category, or pasted listing URL. |
| `maxItems` | integer | `20` | Hard cap on products returned across the whole run. With **Incremental changes for scheduled runs** on, it caps products scanned: unchanged products that are not returned still count toward it. `0` = unlimited (still bounded by Max pages). |
| `proxy` | object | Apify Proxy, residential, country `ES` | Connection settings. See Plan Requirement. |
| `resumeFromRunId` | string | none | A previous run ID or dataset ID: products already collected there are skipped and not charged again. |
| `incrementalMode` | boolean | `false` | Return only new, updated and reappeared products versus the last run of the same search. Adds `changeType`, `changedFields`, `firstSeenAt`, `lastSeenAt`. |
| `stateKey` | string | auto | Names the monitoring campaign (incremental mode only). Empty derives it from the search, filters and detail settings. |
| `emitUnchanged` | boolean | `false` | Also return unchanged products, marked `UNCHANGED` (incremental mode only). |
| `emitExpired` | boolean | `false` | Also return products no longer found, marked `EXPIRED` (incremental mode only; only after a full read of the search). |
| `ignoreFieldsForChanges` | array | none | Field names that must not count as a change (incremental mode only). |
| `mcpConnectors` | array | none | Optional: send a summary of each product to apps you authorized under Integrations. |
| `notionParentPageUrl` | string | none | 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 below are illustrative placeholders, not from a live product.

```json
{
  "productId": "123456",
  "sku": "123456",
  "name": "Sample Cordless Drill 18V",
  "brand": "SAMPLE BRAND",
  "url": "https://www.leroymerlin.es/productos/sample-cordless-drill-18v-123456.html",
  "price": 99.99,
  "priceExclTax": 82.64,
  "currency": "EUR",
  "originalPrice": 129.99,
  "discountAmount": 30.0,
  "discountPercent": 23.1,
  "isOnSpecial": true,
  "rating": 4.7,
  "sellerId": "002",
  "sellerName": "Sample Retailer",
  "sellerType": "1P",
  "totalOfferCount": 12,
  "sellersComposition": "1P_3P",
  "offerType": "NEW",
  "sponsored": false,
  "ean": "4000000000000",
  "description": "Sample product description as shown on the site.",
  "image": "https://media.example.com/sample.jpg",
  "numberOfImages": 5,
  "categoryPath": ["Tools", "Power tools", "Drilling & screwing", "Drills", "Cordless drills"],
  "category": "Cordless drills",
  "stock": 19,
  "stockStatus": "ONSITE",
  "deliveryTime": "1 OPENING_DAY",
  "otherOffers": [
    { "sellerName": "Sample Marketplace Seller", "sellerType": "3P", "price": 105.40, "stock": 3, "deliveryTime": "3 OPENING_DAY" }
  ],
  "numberOfReviews": 275,
  "reviews": {
    "averageRating": 4.7,
    "totalReviewCount": 275,
    "items": [
      { "rating": 5, "body": "Sample review text.", "author": "Sample Customer", "date": "2026-07-15" }
    ]
  },
  "searchMode": "search"
}
```

### Plan Requirement

leroymerlin.es sits behind a strict bot-protection check that refuses plain datacenter and residential connections alike with a short placeholder page, so a bare Apify Proxy setup is not enough on its own. By default the actor uses Apify Proxy with residential access in Spain, and it retries refused pages on a separate alternate connection lane that is slower, roughly 15 to 75 seconds per page. Expect runs to be slower than on a typical shop and success to depend on that lane being available at the time. A datacenter connection is not supported.

If the Apify residential connection is refused for your account (Apify free plans do not include residential proxies), the run prints a boxed warning once and continues on a backup connection, and logs "Served over the backup connection (the default connection was unavailable)." when that connection serves a page. The backup connection for a free plan is a datacenter connection, which this source usually refuses, so expect little or no data without a paid plan or your own proxy URLs. If you supply your own proxy URLs, or switch the proxy off, no backup connection is used.

A run in which no page could be loaded always ends as failed, never as an empty success, with one of these messages:

- "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: https://apify.com/pricing. If you entered your own proxy URLs, check that their credentials are valid." when every connection was refused for your account.
- "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 Leroy Merlin pages. Upgrade at https://apify.com/pricing or supply your own proxy URLs, then try again." when the backup connection was reachable but the site never served a page.
- "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." for any other run where no page loaded.

### FAQ

#### How much does it cost?

This actor uses Apify's pay-per-event pricing. You are charged for each product returned in the dataset, plus a separate, smaller "detail-enrichment" charge: one unit per returned product where detail and/or review enrichment actually ran, whatever the number of reviews returned on it (counted during the run and charged once when the run finishes). Products that incremental mode suppresses as unchanged are neither returned nor charged, plus the standard per-run actor-start charge. The **Pricing** tab on the actor page shows the current rates. Use **Max products** and **Max pages** to cap the size, and cost, of any run.

#### Is it legal to scrape Leroy Merlin?

This actor collects only publicly available product data, prices, offers, ratings and reviews, shown on leroymerlin.es. You are responsible for how you use it: follow Leroy Merlin's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Product names, descriptions and images may carry third-party rights.

#### Can I schedule it, or get only new or changed products?

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental changes for scheduled runs** (`incrementalMode`). The first run returns every matching product as `NEW`; later runs return only `NEW`, `UPDATED` and `REAPPEARED` products, each row carrying `changeType`, `changedFields`, `firstSeenAt` and `lastSeenAt`. Products that did not change are not returned and not charged, even though their page was read to compare them. Because those unchanged products were still scanned, they use up **Max products**: with a small cap, a run can return few or no rows when the first products it scans are unchanged. Turn on `emitUnchanged` or `emitExpired` only if you also want those rows (each returned row is a normal charged result). `EXPIRED` rows are produced only after a run read the whole tracked search, never when **Max products** or **Max pages** cut it short or when Resume was used. All returned fields are compared except the scrape timestamp, the review texts (a new review still moves the rating and review count) and the live stock and delivery-time counters (`stock`, `deliveryTime`, and the same two inside `otherOffers`); extend that list with `ignoreFieldsForChanges`. With incremental mode off the output is unchanged: each run returns the current full snapshot.

#### Why does a product's price sometimes differ from what I saw on the search results page?

When **Fetch product detail** is on (the default), the product's own detail page is the final source for its price, was-price and discount, and it can legitimately disagree with the listing card's price shown a moment earlier (VAT-inclusive versus exclusive display, or a real price change between the two page loads). The dataset always reflects the detail page's value once detail is fetched, and the run's price sort is re-applied after that refresh within each search or pasted listing link, so sort order and the price you see never disagree there.

#### Why did my run fail instead of returning an empty dataset?

A page that genuinely does not exist (bad category path, mistyped product slug, delisted listing) comes back as a normal "not found" and is accepted right away. A page that returns the site's placeholder check page instead of content is treated as a refusal, not as an empty result, and is retried with a fresh session, then on the alternate connection lane, then on the backup connection when one is available to your run. If a product's detail page still cannot be read, the product is kept with its listing data only and is not charged for enrichment. If no page at all could be read, the run fails with one of the three messages quoted in the Plan Requirement section rather than ending as an empty success. A run where every request succeeded but nothing matched your keyword, category or filters ends normally with zero items and the status message "No products matched".

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

Yes. Call it from any Apify integration or MCP client, and use the **Pipe results into your apps** field to push a summary of each product into Notion, Linear, Airtable or another Apify MCP connector, alongside the full dataset.

### 🔗 Want more electronics data?

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

<table>
<tr><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><td>🔌 <a href="https://apify.com/abotapi/darty-com-scraper"><b>Darty Scraper</b></a><br>Scrape Darty (darty.com) products: current price plus strike-through reference price and...</td></tr>
<tr><td>👗 <a href="https://apify.com/abotapi/otto-de-scraper"><b>OTTO.de Scraper</b></a><br>Scrape OTTO.de products by search or URL. Extract prices, discounts, variants...</td><td>🔌 <a href="https://apify.com/abotapi/fnac-com-scraper"><b>Fnac Scraper</b></a><br>Scrape Fnac (fnac.com) products: current price plus strike-through discount, colour/model...</td></tr>
<tr><td>🛒 <a href="https://apify.com/abotapi/mediamarkt-es-scraper"><b>MediaMarkt.es Scraper</b></a><br>Scrape MediaMarkt Spain (mediamarkt.es) products: current price, strike-through original...</td><td>🛒 <a href="https://apify.com/abotapi/decathlon-es-scraper"><b>Decathlon.es</b></a><br>Scrape Decathlon Spain (decathlon.es) sporting goods: current price plus strike-through...</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/leroymerlin-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' finds products by keyword and/or a leroymerlin.es category path, with price/rating/promotion/seller filters and the site's own sort. 'url' scrapes any product or listing (search/category) page URL you paste, walking pagination forward automatically.

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

Free-text keyword, e.g. 'taladro' or 'grifo cocina'. Leave empty and use Category only below to browse a category without a keyword.

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

A leroymerlin.es catalog category path (copy it from the site's own breadcrumb links or a category URL), a plain category name such as 'Taladros de batería' also works but only if it matches the site's own slug; a name that matches no listing logs a warning. Example path: 'herramientas/herramientas-electricas-portatiles/herramientas-electricas-para-taladrar-atornillar/taladros/taladros-de-bateria'. Ignored when Search keyword is set -- fill in exactly one of the two.

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

leroymerlin.es's own sort, applied whether you run a keyword/category search or paste a listing URL. The site keeps one sponsored slot pinned regardless of sort; every other item comes back correctly ordered by your chosen sort.

## `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 whose average rating is at least this many stars (0-5). Rating is already present on every listing card, no detail fetch required.

## `onPromotionOnly` (type: `boolean`):

Keep only products currently showing the site's own strike-through original price and discount.

## `sellerFilter` (type: `string`):

Narrow to products sold directly by Leroy Merlin, or only marketplace (third-party) offers. 'Any' keeps both.

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

Only used when mode = url -- ignored in every other mode. Paste one or more product pages (under /productos/\*-<id>.html), category listing pages (under /productos/<path>/), or search result pages (/search?q=...). Pagination continues forward automatically from any ?p= already present. Left untouched, this field scrapes one example of each shape (a keyword search, a category listing and a product page).

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

Adds EAN, description, hero image, breadcrumb category path, stock/delivery, and every competing marketplace offer (price, stock, delivery per seller) from the product page. Current price, was-price/discount, rating and seller identity are already included without this toggle.

## `fetchReviews` (type: `boolean`):

Fetch the product's own reviews (not a third-party vendor): overall rating, total review count, and the page's most recent individual reviews (rating, body, author, date). leroymerlin.es itself only ever surfaces its most recent ~8 reviews per product; the rating and total count reflect the full history. Shares the same page fetch as 'Fetch product detail' -- turning both on costs no extra request. Products with no reviews return an empty list.

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

Cap on reviews returned per product when 'Fetch reviews' is on, up to the site's own page limit (the product page embeds only its most recent ~8 reviews; no paginated review endpoint is exposed).

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

Stop after this many result pages per search, category, or pasted listing URL. Leave empty for no page limit -- the run stops at Max products.

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

Hard cap on products returned across the whole run. With incremental mode on it caps products scanned: unchanged products that are not returned still count toward it. 0 = unlimited (still bounded by Max pages).

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

Apify Proxy is required. leroymerlin.es answers only on a Spanish residential connection, so the run uses a residential exit in Spain by default.

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

Paste a previous run ID or dataset ID to continue an interrupted pull without returning or charging for products already collected there. For recurring monitoring of the same search, use Incremental changes below instead.

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

Turn this on for daily or recurring monitoring. The first run returns every matching product as NEW. Later runs return only NEW, UPDATED and REAPPEARED products (a price cut, a stock change, a new seller offer). Each row gets changeType, changedFields, firstSeenAt and lastSeenAt. Compared are all returned fields except the scrape timestamp, the review texts (a new review still moves the rating and review count) and the live stock and delivery-time counters. State is kept separately for each search, filter and detail setup; use State key to name or share a campaign. Turn on Emit unchanged or Emit expired only if you also want those rows returned (and billed).

## `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 derive a key automatically from the search, filters and detail settings.

## `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, rows you already have.

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

Off by default. Turn on to also return products that were present in a previous run but are no longer found, marked EXPIRED. Only produced after a run fully scanned the tracked search, so never when Max products or Max pages cut it short, or when Resume was used. This returns, and bills, synthetic rows.

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

Field names (for example sponsored) that must not make a product count as UPDATED. They are still returned in the row. Dotted paths such as otherOffers.stock are allowed.

## `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": "taladro",
  "sortBy": "relevance",
  "onPromotionOnly": false,
  "sellerFilter": "any",
  "urls": [
    "https://www.leroymerlin.es/search?q=taladro",
    "https://www.leroymerlin.es/productos/herramientas/herramientas-electricas-portatiles/herramientas-electricas-para-taladrar-atornillar/taladros/taladros-de-bateria/",
    "https://www.leroymerlin.es/productos/taladro-percutor-a-bateria-bosch-18v-2-baterias-easyimpact-18v-40-83518470.html"
  ],
  "fetchDetails": true,
  "fetchReviews": true,
  "maxReviewsPerProduct": 20,
  "maxItems": 20,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "ES"
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 50
}
```

# Actor output Schema

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

No description

## `otherOffers` (type: `string`):

No description

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

No description

## `changes` (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": "taladro",
    "urls": [
        "https://www.leroymerlin.es/search?q=taladro",
        "https://www.leroymerlin.es/productos/herramientas/herramientas-electricas-portatiles/herramientas-electricas-para-taladrar-atornillar/taladros/taladros-de-bateria/",
        "https://www.leroymerlin.es/productos/taladro-percutor-a-bateria-bosch-18v-2-baterias-easyimpact-18v-40-83518470.html"
    ],
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "ES"
    },
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "ignoreFieldsForChanges": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/leroymerlin-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": "taladro",
    "urls": [
        "https://www.leroymerlin.es/search?q=taladro",
        "https://www.leroymerlin.es/productos/herramientas/herramientas-electricas-portatiles/herramientas-electricas-para-taladrar-atornillar/taladros/taladros-de-bateria/",
        "https://www.leroymerlin.es/productos/taladro-percutor-a-bateria-bosch-18v-2-baterias-easyimpact-18v-40-83518470.html",
    ],
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "ES",
    },
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "ignoreFieldsForChanges": [],
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/leroymerlin-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": "taladro",
  "urls": [
    "https://www.leroymerlin.es/search?q=taladro",
    "https://www.leroymerlin.es/productos/herramientas/herramientas-electricas-portatiles/herramientas-electricas-para-taladrar-atornillar/taladros/taladros-de-bateria/",
    "https://www.leroymerlin.es/productos/taladro-percutor-a-bateria-bosch-18v-2-baterias-easyimpact-18v-40-83518470.html"
  ],
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "ES"
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": []
}' |
apify call abotapi/leroymerlin-es-scraper --silent --output-dataset

```

## MCP server setup

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