# Castorama.fr - Products, Prices & Reviews (`abotapi/castorama-fr-scraper`) Actor

Scrape Castorama.fr DIY and home-improvement products by keyword, category, or URL. Extract prices, original prices, promotions, seller details, categories, specifications, ratings, and customer reviews. Incremental mode tracks product and price changes over time.

- **URL**: https://apify.com/abotapi/castorama-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.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/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

## Castorama France Product Scraper

Scrape products from **Castorama France** (`castorama.fr`) — the French DIY /
home-improvement retailer. Search by keyword or category, or paste product /
category / search links. Every product comes back as one flat record with
price, was-price, promotion, seller (Castorama or a marketplace partner),
breadcrumb category and rating; enable details to add the full description,
technical specifications, and customer reviews.

### What you can do

- **Search by keyword or category** — e.g. `perceuse` (drill), `peinture
  blanche mate` (matte white paint) — with brand, price, promotion, seller
  and rating filters, plus sorting. A keyword that matches a real Castorama
  category (like `perceuse`) resolves straight to that category's full
  facet set, exactly like typing into the site's own search box.
- **Paste links** — mix product links, category links and search-result
  links in one list and let the actor route each entry. Product links
  return full detail. Promotion/in-stock/sort still apply to every link;
  brand/price/rating also narrow a pasted category or search-result link,
  just not a single pasted product link.
- **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 |
|---|---|
| **Mode** | `search` or `url`. |
| **Search keywords or category links** | Keywords or Castorama category links to search/browse (search mode only). Each is walked separately. |
| **Brand** | Only keep products of this brand. Narrows a search, or a pasted category/search-result link in url mode; has no effect on a pasted product link. |
| **Only sold directly by Castorama** | Exclude marketplace (third-party seller) offers. Same scope as Brand above. |
| **Min / Max price (EUR)** | Keep only products within a price range. Same scope as Brand above. |
| **Minimum average rating** | Keep only products rated at least N stars. Same scope as Brand above. |
| **Only products on promotion** | Keep only products flagged with a real promotion. Applies to every result in either mode, including a single pasted product link. |
| **Only purchasable products** | Keep only products the site currently marks purchasable (client-side; see Notes). Same scope as "Only products on promotion" above. |
| **Sort by** | Relevance, price ↑/↓, rating ↓, newest, or best sellers — Castorama's own sort options. Same scope as "Only products on promotion" above. |
| **Castorama France links** | Product links, category links and search-result links to scrape directly, mixed freely (url mode). Product links return full detail. |
| **Fetch product details** | Add description, technical specifications and customer reviews. 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 entry. 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. |
| **Incremental changes for scheduled runs** | Daily/recurring monitoring. 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 search/filter/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 no longer found, marked `EXPIRED`, once a run has fully scanned the tracked search. Adds and bills extra synthetic rows. |
| **Proxy** | Apify Proxy — datacenter by default (see Proxy & connection below). |
| `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. With details off you get the listing‑level fields;
with details on you also get description, specifications and reviews.
Example (illustrative values):

```json
{
  "productId": "3666294006226",
  "ean": "3666294006226",
  "title": "Perceuse Visseuse 20V Exemple + 2 Batteries 1,5 Ah + Chargeur rapide + Malette",
  "brand": "MarqueExemple",
  "price": 79.99,
  "currency": "EUR",
  "originalPrice": 139.99,
  "discountAmount": 60.0,
  "discountPercentage": 43,
  "isOnSpecial": true,
  "promotionLabel": "Promotion",
  "sellerId": "2131",
  "sellerName": "Vendeur Partenaire Exemple",
  "soldByCastorama": false,
  "category": "Visseuse dévisseuse",
  "categoryPath": [
    { "name": "Visseuse dévisseuse", "path": "/visseuse-devisseuse/cat_id_0003519.cat" },
    { "name": "Perceuse visseuse", "path": "/perceuse-visseuse/cat_id_0003518.cat" }
  ],
  "availability": { "status": "Available", "purchasable": true, "homeDelivery": "Available" },
  "images": ["https://media.castorama.fr/is/image/Castorama/example.jpg"],
  "image": "https://media.castorama.fr/is/image/Castorama/example.jpg",
  "url": "https://www.castorama.fr/mkp/example/3666294006226_CAFR.prd",
  "specifications": [{ "name": "Marque", "value": "MarqueExemple" }, { "name": "Tension (en V)", "value": "20V" }],
  "rating": 4.75,
  "reviewCount": 4,
  "reviews": [
    { "author": "Exemple", "title": "Efficace", "body": "Facile à utiliser.", "rating": 5, "date": "2026-07-08T15:18:22.000+00:00" }
  ],
  "searchMode": "search",
  "scrapedAt": "2026-01-01T00:00:00Z"
}
```

**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 |

### Was-price and promotions — verified live, not assumed

Fetched real listing and product-detail pages via `https://www.castorama.fr/search?term=perceuse`
(redirects to the "Perceuse" category, `cat_id_3796`) and its individual products,
e.g. `https://www.castorama.fr/mkp/perceuse-visseuse-20v-nemura-2-batteries-1-5-ah-chargeur-rapide-malette-avec-54-accessoires/3666294006226_CAFR.prd`,
2026-08-03. The site's own catalog payload carries a genuine prior price for
the same item: `pricing.wasPrice` (was €139.99) alongside `pricing.currentPrice`
(now €79.99) and `pricing.saving` (the €60.00 amount saved), and every such
item is explicitly flagged `merchandisingTag: {"text": "Promotion"}`. The
product-detail offer additionally carries `discountStartDate`/`discountEndDate`
(a real promo window, e.g. 2026‑08‑01 → 2026‑09‑01) and matching
`originPrice`/`wasPrice`, confirming this is a real markdown rather than a
competitor-price or unit-price artifact. Castorama also exposes a real facet
for this in its own navigation — **"Type d'offre = Promotion"** —
returned by the same payload's `filtering.filters[]`, which this actor maps
to `onPromotionOnly`. `isOnSpecial`/`originalPrice`/`discountAmount`/
`discountPercentage` are only ever populated when this genuine was-price is
present; a cheaper marketplace offer or a unit-price spread never sets them.

### Reviews — verified live, not assumed

The product detail route (`.../3666294006226_CAFR.prd`) embeds a `reviews[]`
array directly in its own server-rendered payload — real customer rows with
`UserNickname`/`Title`/`ReviewText`/`Rating`/`SubmissionTime` (mapped here to
`author`/`title`/`body`/`rating`/`date`). The product's `averageRating.count`
matched the number of reviews actually returned on every product checked
live. No third-party review host is ever called directly by this actor — the
review data ships first-party in Castorama's own page payload (same pattern
independently confirmed on a sibling French retailer). When a product has no
reviews, `reviews` is `[]` and `rating`/`reviewCount` are `null`/`0` — never
fabricated. **Not yet observed live:** a product whose true review count
exceeds what ships inline (all products checked had ≤4 reviews, all
returned); if Castorama caps the inline array at a higher count than that on
some products, this actor currently returns only what the payload includes.

### 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` |
| Run the same 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.

**Incremental mode** (`incrementalMode`) is for a schedule (for example, daily): the actor remembers the previous run of the *same* 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). Turn on `emitUnchanged` or `emitExpired` only when you also want those rows returned (and billed for). State is isolated per search/URL and filter/detail-mode setup automatically; set `stateKey` to name or deliberately share a monitoring campaign.

Scheduled-run example — same search, run daily:

Day 1 (first run ever for this search):

```json
{ "mode": "search", "queries": ["perceuse"], "incrementalMode": true }
```

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

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

```json
{ "mode": "search", "queries": ["perceuse"], "incrementalMode": true }
```

→ products whose price/promotion/stock/etc. changed 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 **strongly recommended** but not geography-gated. Verified
live 2026-08-03: the storefront's edge randomly serves a static
"Site en maintenance" placeholder (HTTP 503) to a share of connections —
observed on Apify **datacenter** exits, Apify **residential** exits (both
French and US), *and* real requests with no relation to exit country. This
is a rate/reputation gate on the edge, not a geo-block: no country pin is
required, and the same tier that fails one request succeeds the next. The
actor retries a blocked fetch on a fresh sticky proxy session, capped at 5
attempts. **Datacenter** is the default (cheaper); switch to
**Residential** in the Proxy field only if a run sees persistent failures.

### 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.
- `searchMode` on each record reports how it was found: `search`,
  `category`, or `product` when it came from a pasted product link.
- Prices are in **EUR**.
- Duplicate products (same EAN) are returned once per run.
- `soldByCastorama` distinguishes a first-party Castorama offer from a
  marketplace (third-party) seller — `sellerName`/`sellerId` carry the
  marketplace seller's identity when it's not Castorama.
- "Only purchasable products" is a client-side filter on each product's own
  status; Castorama's in-store stock facet needs a shopper's postal code
  and is not covered by this actor.

# Actor input Schema

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

Choose 'search' for keywords/categories with filters, or 'url' to scrape pasted Castorama France links — product links, category links and search-result links are all accepted.

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

One or more keywords (e.g. 'perceuse', 'peinture blanche mate') or Castorama category page links, each searched/walked separately. A category link ends in cat\_id\_<digits>.cat, for example https://www.castorama.fr/outillage/outillage-electroportatif/perceuse-visseuse-perceuse-a-percussion-et-tournevis-sans-fil/cat\_id\_3796.cat

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

Optional. Only keep products of this brand, matched against Castorama's own 'Marque' facet (e.g. 'Bosch Professional', 'Makita', 'DeWalt').

## `sellerCastoramaOnly` (type: `boolean`):

Optional. Exclude marketplace (third-party seller) offers — Castorama's own 'Vendu par = Castorama' facet.

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

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

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

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

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

Optional. Only keep products whose average rating is at least this many stars — Castorama's own 'Évaluation' facet.

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

Only used when mode = url — ignored in search mode. Mix freely: product pages ending in .prd, category pages ending in cat\_id\_<digits>.cat, or search-result links like /search?term=.... Multiple entries supported.

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

Optional. Keep only products currently flagged with a real promotion (a genuine prior price for the same item) — Castorama's own 'Type d'offre = Promotion' facet.

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

Optional. Keep only products the site currently marks as purchasable online. Applied client-side against each product's own status (Castorama's in-store stock facet needs a postal code and is not covered).

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

How to order results — Castorama's own sort options.

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

Collect each product's full detail: description, technical specifications, and customer reviews (author, title, body, rating, date). Turn off for a faster, lighter run that returns only the listing-level fields (title, brand, price, was-price, seller, breadcrumb, rating summary).

## `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. 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 for each search/URL and filter/detail setup; use State key when you want 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 search/URL and filter/detail settings — different 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 — a rotating connection so retries can route around the storefront's occasional maintenance placeholder. Datacenter by default.

## `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
{
  "mode": "search",
  "queries": [
    "perceuse"
  ],
  "sellerCastoramaOnly": false,
  "minRating": "0",
  "urls": [
    "https://www.castorama.fr/mkp/perceuse-visseuse-20v-nemura-2-batteries-1-5-ah-chargeur-rapide-malette-avec-54-accessoires/3666294006226_CAFR.prd",
    "https://www.castorama.fr/outillage/outillage-electroportatif/perceuse-visseuse-perceuse-a-percussion-et-tournevis-sans-fil/cat_id_3796.cat"
  ],
  "onPromotionOnly": false,
  "inStockOnly": false,
  "sortBy": "relevance",
  "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 = {
    "mode": "search",
    "queries": [
        "perceuse"
    ],
    "urls": [
        "https://www.castorama.fr/mkp/perceuse-visseuse-20v-nemura-2-batteries-1-5-ah-chargeur-rapide-malette-avec-54-accessoires/3666294006226_CAFR.prd",
        "https://www.castorama.fr/outillage/outillage-electroportatif/perceuse-visseuse-perceuse-a-percussion-et-tournevis-sans-fil/cat_id_3796.cat"
    ],
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "proxy": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/castorama-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 = {
    "mode": "search",
    "queries": ["perceuse"],
    "urls": [
        "https://www.castorama.fr/mkp/perceuse-visseuse-20v-nemura-2-batteries-1-5-ah-chargeur-rapide-malette-avec-54-accessoires/3666294006226_CAFR.prd",
        "https://www.castorama.fr/outillage/outillage-electroportatif/perceuse-visseuse-perceuse-a-percussion-et-tournevis-sans-fil/cat_id_3796.cat",
    ],
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "proxy": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/castorama-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 '{
  "mode": "search",
  "queries": [
    "perceuse"
  ],
  "urls": [
    "https://www.castorama.fr/mkp/perceuse-visseuse-20v-nemura-2-batteries-1-5-ah-chargeur-rapide-malette-avec-54-accessoires/3666294006226_CAFR.prd",
    "https://www.castorama.fr/outillage/outillage-electroportatif/perceuse-visseuse-perceuse-a-percussion-et-tournevis-sans-fil/cat_id_3796.cat"
  ],
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true
  }
}' |
apify call abotapi/castorama-fr-scraper --silent --output-dataset

```

## MCP server setup

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