# E.Leclerc Scraper: Local Prices, Stock & Promotions (`abotapi/leclerc-fr-scraper`) Actor

Scrape E.Leclerc France (e.leclerc) grocery and retail products. Search by keyword or category, or paste product and category links. Pick a store by postal code so prices and stock are the real ones for that store. Returns price, unit price, stock, promotions, rating and images.

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

## Pricing

from $1.20 / 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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## E.Leclerc Product Scraper

Scrape products from **E.Leclerc France** (`e.leclerc`) — the grocery / general
retail catalogue. Search by keyword/category, or paste product / category /
search links. Every product comes back as one flat record with title,
description, images, store-scoped price, unit price, a genuine promotion (never
a loyalty cashback disguised as one), stock and — where the storefront has one
— an aggregate rating.

### ⚠️ E.Leclerc is store-scoped — read this first

Every E.Leclerc store prices, promotes and stocks its own catalogue
independently. This actor **requires you to choose one store** — by postal
code or by an exact store sign code — and every result in a run reflects
**that one store**. There is no "national price": two different stores can
(and often do) show a different price, promotion, or stock level for the
exact same product. Every record also carries `storeSignCode`, `storeName`
and `storePostalCode` so results are always traceable to a specific store.

### What you can do

- **Search by keyword or category** — e.g. `lait` (milk), `lessive` (laundry
  detergent) — with price, promotion, stock and rating filters, plus sorting.
  A keyword that matches one of the storefront's own categories is served
  that category's full listing automatically. You can also paste one or more
  category links directly.
- **Paste links or EANs** — mix product links, bare EAN barcodes, category
  links and search-result links in one list and let the actor route each
  entry.
- **Resume & recurring updates** — turn on Incremental mode to get only NEW,
  UPDATED, and REAPPEARED products on every scheduled run, or resume one
  specific interrupted crawl with `resumeFromRunId`.

### Input

| Field | Description |
|---|---|
| **Postal code** | A French postal code. The run picks the nearest Hyper/Super/Express E.Leclerc store to it. |
| **Store sign code** | Optional exact-store override (e.g. copy `storeSignCode` from a previous run's output to reproduce it exactly). Wins over Postal code when both are set. |
| **Mode** | `search` or `url`. |
| **Search keywords** | Keywords to search (search mode). |
| **Category links** | E.Leclerc category page links to browse, e.g. `https://www.e.leclerc/cat/lessives-marque-repere` (search mode). |
| **E.Leclerc links or EANs** | Product links, bare EAN barcodes, category links, and search-result links to scrape directly, mixed freely (url mode). |
| **Sort by** | Relevance, price ↑/↓, unit price ↑/↓, or name. Applied over the collected products. |
| **Minimum average rating** | Keep only products rated at least N stars. |
| **Only on promotion** | Keep only products with a genuine reduced price at your store (never a loyalty cashback). |
| **Only in stock** | Keep only products currently in stock at your store. |
| **Min / Max price (EUR)** | Keep only products within a price range, at your store. |
| **Fetch product details** | Fetch each product's page for a human-readable category breadcrumb. Title, description, images, price, unit price, promotion, stock and rating are always included regardless of this toggle. On by default. |
| **Max products** | The run's cap on how many products to return. Default 20; `0` = unlimited. |
| **Max pages per keyword / category / link** | Result pages walked per keyword / category / link. Default `0` = unlimited — walks every page until *Max products* is hit or a page repeats no new products. |
| **Resume from a previous run** | Continue one specific previous run/dataset: products already collected there are skipped. For recurring daily monitoring, use Incremental mode instead — see "Resume & recurring updates" below. |
| **Incremental changes for scheduled runs** | Daily/recurring monitoring of this same store + search. First run returns everything as `NEW`; later runs return only `NEW`/`UPDATED`/`REAPPEARED` by default. See "Resume & recurring updates" below. |
| **State key** | Optional name for a monitoring campaign. Auto-derived from your store/search/detail settings when left empty. |
| **Emit unchanged products** | Incremental mode only. Also return products unchanged since the last run, marked `UNCHANGED`. Adds and bills extra rows. |
| **Emit expired products** | Incremental mode only. Also return products from a previous run no longer found, marked `EXPIRED`, once a run has fully scanned the search. Adds and bills extra synthetic rows. |
| **Proxy** | Apify Proxy — a datacenter exit by default, which is the cheaper tier. |
| `mcpConnectors` | Optional MCP connectors to export results into (Notion, Linear, Airtable, Apify). |
| `notionParentPageUrl` | Notion connector only: page under which item pages are created. |
| `maxNotifyListings` | Cap on items exported to each connector per run. Does not affect the dataset. |

### Output

One record per product, scoped to your chosen store. Example (illustrative
values):

```json
{
  "productId": "3700281702286",
  "ean": "3700281702286",
  "title": "ULTRA-HYDRATANT Lait Corps 500 ml",
  "brand": "Topicrem",
  "description": "L'Ultra-Hydratant lait corps Topicrem est une émulsion onctueuse...",
  "images": ["https://media.e.leclerc/3700281702286_1"],
  "categoryPath": "Crèmes hydratantes/nourrissantes > Topicrem > Les Essentiels",
  "price": 11.04,
  "currency": "EUR",
  "unitPrice": null,
  "wasPrice": null,
  "onPromotion": false,
  "loyaltyCashbackPercent": 30.0,
  "loyaltyCashbackLabel": "Ticket E.Leclerc avec la carte",
  "inStock": true,
  "stock": 537,
  "rating": 4.8,
  "reviewCount": 28,
  "reviews": [],
  "storeSignCode": "1375",
  "storeName": "Montparnasse",
  "storePostalCode": "75014",
  "canonicalUrl": "https://www.e.leclerc/fp/ultra-hydratant-lait-corps-500-ml-3700281702286",
  "searchMode": "search",
  "scrapedAt": "2026-08-03T00:00:00Z"
}
```

**Was-price, discounts and loyalty cashback.** `wasPrice`/`discountAmount`/
`discountPercent` are populated only when the storefront itself carries a
genuine prior price for that exact item — they are never derived from a
"Ticket E.Leclerc avec la Carte" style **loyalty cashback** (paid in full
now, a percentage credited back later on your loyalty card, not a price
cut) or from the storefront's own `original-price` field, which was
observed to be an unreliable reference value (not a genuine prior price on
the products checked). A genuine loyalty cashback still surfaces —
separately, as `loyaltyCashbackPercent` / `loyaltyCashbackLabel` — but never
sets `onPromotion` or a was-price. On the products sampled while building
this actor, no genuine was-price/markdown was found live; this is reported
honestly (`wasPrice: null`) rather than fabricated.

**Nutrition / ingredients / specifications.** E.Leclerc's product data does
not expose a dedicated nutrition table, ingredients list, or specifications
block anywhere in the routes this actor uses (verified across multiple
product categories). Composition text, when a product's description
mentions it, is part of the free-text `description` field.

**Ratings & reviews.** Products carry a genuine aggregate rating (`rating`,
`reviewCount`) where the storefront has one. Individual customer review
*text* is served by a client-side widget this actor does not drive, so the
`reviews` array is present but empty on rated products, and rating fields
are simply absent on products with no rating (never fabricated zeros).

**Incremental mode only.** When `incrementalMode` is on, every returned
record also carries:

| Field | Description |
|---|---|
| `changeType` | `NEW` | `UPDATED` | `UNCHANGED` | `REAPPEARED` | `EXPIRED` |
| `changedFields` | Top-level fields that changed since last seen; non-empty only for `UPDATED` |
| `firstSeenAt` | When this product was first observed by this monitoring campaign |
| `lastSeenAt` | When this product was last observed |

### Resume & recurring updates

There are two different things here — pick the one that matches what you're doing:

| Need | Use |
| --- | --- |
| A crawl stopped and should continue | `resumeFromRunId` / automatic checkpoint recovery |
| Run the same store + search every day and receive only changes | `incrementalMode` |
| Keep separate daily campaigns for similar searches | distinct `stateKey` values |
| Run a normal full snapshot | leave both off |

**Resume** (`resumeFromRunId`) continues one specific interrupted or previous large crawl: paste a run ID or dataset ID and this run skips products already collected there, returning only the remaining new products. An automatic same-run checkpoint also protects against platform migrations/Resurrects without any input needed.

**Incremental mode** (`incrementalMode`) is for a schedule (for example, daily): the actor remembers the previous run of the *same store + search* by itself, so you never paste a run ID. The first run returns everything as `NEW`. Later runs return only `NEW`, `UPDATED`, and `REAPPEARED` products by default — duplicates and unchanged products are suppressed (and not charged). A different store, even with the identical search, always gets its own state — prices/stock at one store never look like a "change" at another. Set `stateKey` to name or deliberately share a monitoring campaign.

Scheduled-run example — same store + search, run daily:

Day 1 (first run ever for this search):

```json
{ "postalCode": "75014", "mode": "search", "queries": ["lait"], "incrementalMode": true }
```

→ every product comes back with `"changeType": "NEW"`.

Day 2 (the schedule fires again, identical input):

```json
{ "postalCode": "75014", "mode": "search", "queries": ["lait"], "incrementalMode": true }
```

→ products whose price/promotion/stock/etc. changed at that store come back as `"changeType": "UPDATED"` with `changedFields` listing what changed, brand-new products come back as `"changeType": "NEW"`, products that vanished and came back come back as `"changeType": "REAPPEARED"` — and products that are still there, unchanged, are **not** returned at all (suppressed, not charged) unless `emitUnchanged` is on.

### Proxy & connection

Apify Proxy is optional. A **datacenter** connection is the default because
it is the cheaper tier and is sufficient here — you do not need to enable
residential for this actor.

### 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. Supported connectors: Notion, Linear, Airtable, and Apify. Leave the field empty to skip; it never changes the dataset output.

### Notes

- Search and category result pages are walked forward one page at a time until
  *Max products* is reached, the storefront runs out of pages, or (with the
  default unlimited *Max pages*) a page repeats no new products.
- A search keyword that the storefront recognizes as a category is followed to
  that category's own listing automatically (the storefront's own behavior);
  the returned `searchMode` still reports how you asked for it.
- Long runs survive an Apify server migration or a manual Resurrect: progress
  is checkpointed, so a resumed run continues without re-scraping or
  double-charging products already collected.
- `searchMode` on each record reports how it was found: `search`, `category`,
  or `product` when it came from a pasted product link or EAN.
- Prices are in **EUR**. Duplicate products (same EAN) are returned once per run.

# Actor input Schema

## `postalCode` (type: `string`):

A French postal code (e.g. 75014). The run picks the nearest Hyper/Super/Express E.Leclerc store to this postal code and uses its prices, promotions and stock for every result. Ignored if Store sign code is also set.

## `storeSignCode` (type: `string`):

Optional. Pin an exact E.Leclerc store by its 4-digit sign code instead of resolving one from a postal code -- useful to reproduce a previous run exactly (every result carries its store's sign code as storeSignCode, so you can copy it from there). Overrides Postal code when set.

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

Choose 'search' for keywords and/or category links with filters, or 'url' to scrape pasted E.Leclerc links -- product links, category links, search-result links and bare product barcodes (EAN) are all accepted.

## `queries` (type: `array`):

One or more keywords to search, for example 'lait' (milk) or 'lessive' (laundry detergent). Each keyword is searched separately. A keyword that matches one of the storefront's own categories is served that category's full listing automatically (e.g. 'lessive' resolves to the Laundry detergents category).

## `categories` (type: `array`):

One or more E.Leclerc category page links, each walked separately across its result pages. To get one: open e.leclerc, browse to the category you want, and copy the address -- a category address looks like https://www.e.leclerc/cat/lessives-marque-repere .

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

Only used when mode = url -- ignored in search mode. Mix freely: product pages under /fp/<slug>-<ean>, category pages under /cat/<slug>, search-result pages under /recherche?q=..., or bare EAN barcodes such as 3518646124167. Multiple entries supported.

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

How to order the collected products. Applied over the products gathered in the run before they are returned.

## `minRating` (type: `string`):

Optional. Only keep products whose average rating is at least this many stars. Products with no rating are excluded when this is set.

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

Optional. Keep only products with a genuine reduced price versus a real prior price at your store. A "Ticket E.Leclerc" loyalty cashback (paid back later on your card, not a price cut) never counts as a promotion here.

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

Optional. Keep only products currently in stock at your chosen store.

## `minPrice` (type: `integer`):

Optional. Only keep products priced at or above this amount at your store.

## `maxPrice` (type: `integer`):

Optional. Only keep products priced at or below this amount at your store.

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

Fetch each product's detail page for a human-readable category breadcrumb. Title, description, images, price, unit price, promotion, stock and rating are always included regardless of this toggle -- they come from the same store-scoped call as price, at no extra cost. Turn on for the friendlier breadcrumb; turn off for a faster run when you don't need it.

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

Maximum number of products to return across the whole run. This is the run's cap. Use 0 for unlimited.

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

Maximum result pages walked per keyword / category / link. 0 = unlimited (walk all pages) -- the run then stops only at Max products or when a page repeats no new products.

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

Paste a previous run ID or dataset ID to continue a large crawl of products without returning or charging for products already collected there. Use this after an interrupted run, or when continuing a product catalogue pull in another run. For recurring daily 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 normally return only NEW, UPDATED, and REAPPEARED products. Turn on "Emit unchanged" or "Emit expired" only when you also want those products returned (and billed). State is kept separately per store, search/category/URL and detail-mode setup -- use State key to name or deliberately share a monitoring campaign. To continue one specific interrupted run instead, use Resume from a previous run above.

## `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 store/search/category/URL and detail settings -- different stores or searches 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 found, marked EXPIRED. Only produced once a run has fully scanned the tracked search -- not when Max products capped it or when Resume was used. This returns -- and bills -- extra synthetic rows, so leave it off unless you need expiry tracking.

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

Apify Proxy configuration. The actor connects through a datacenter exit by default; residential is optional and not required.

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

Optionally send results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize one under Apify, Settings, API & Integrations, then select it here. Notion gets a rich page-per-item export; other connectors get a best-effort write/digest. Leave empty to skip; never changes the dataset output. 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
{
  "postalCode": "75014",
  "mode": "search",
  "queries": [
    "lait"
  ],
  "urls": [
    "https://www.e.leclerc/cat/lessives-marque-repere"
  ],
  "sortBy": "relevance",
  "minRating": "0",
  "onPromotionOnly": false,
  "inStockOnly": false,
  "fetchDetails": true,
  "maxItems": 20,
  "maxPages": 0,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true
  },
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (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 = {
    "postalCode": "75014",
    "mode": "search",
    "queries": [
        "lait"
    ],
    "urls": [
        "https://www.e.leclerc/cat/lessives-marque-repere"
    ],
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "proxy": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/leclerc-fr-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 = {
    "postalCode": "75014",
    "mode": "search",
    "queries": ["lait"],
    "urls": ["https://www.e.leclerc/cat/lessives-marque-repere"],
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "proxy": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/leclerc-fr-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 '{
  "postalCode": "75014",
  "mode": "search",
  "queries": [
    "lait"
  ],
  "urls": [
    "https://www.e.leclerc/cat/lessives-marque-repere"
  ],
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true
  }
}' |
apify call abotapi/leclerc-fr-scraper --silent --output-dataset

```

## MCP server setup

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