# HORNBACH Products, Prices & Reviews Scraper (`abotapi/hornbach-de-scraper`) Actor

Scrape HORNBACH (hornbach.de) DIY, building & garden products: current + strike-through price with discount, per-unit pricing (m2/kg/piece), full technical specs, category path, online & in-store availability, and complete product reviews with rating breakdown.

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

## Pricing

from $1.10 / 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

## Hornbach.de Scraper: Prices, Stock, Specs and Reviews

Hornbach.de Scraper turns Hornbach.de, the German DIY, building materials, and garden retailer, into a clean product data feed. Get the current price alongside the manufacturer's suggested price and computed discount when a product is marked down, per-square-metre/per-kilogram/per-piece pack pricing where the site shows it, full technical specifications, breadcrumb category paths, online and in-store availability, and complete customer review histories with a star-rating breakdown. Search by keyword and/or category with the site's own filters, or paste product, search, and category links 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.** When Hornbach.de shows both its own price and a manufacturer's suggested price, both are captured, plus the computed discount amount and percentage, not just a badge.
- **Unit pricing for bulk goods.** Tiles, flooring, and similar products sold by quantity return their per-square-metre, per-kilogram, or per-piece pack price alongside the item price, exactly as shown on the product page.
- **Full technical specifications on demand.** Turn on full product detail for the complete breadcrumb category path, EAN, media gallery, variant matrix, and every characteristic the product page lists, returned as clean key and value pairs.
- **Complete review history, fetched by default.** Overall rating, review count, a full one to five star rating breakdown, and every individual review with author, date, rating, title, and body text.
- **Online and in-store availability together.** Home delivery status alongside store pickup or reservation status and seller information, plus exact aisle-level stock text when full detail is on.
- **Three ways in.** Keyword and/or category search with the site's real filters, or paste product, search, or category links and let pagination continue automatically.
- **Built for schedules.** Incremental mode tracks one search over time and returns only what changed on later runs, or resume one interrupted run by pasting its run or dataset id.

### Use Cases

- **Price and discount monitoring:** track the gap between the manufacturer's suggested price and Hornbach.de's own price, plus per-unit pricing, for repricing or comparison-shopping tools.
- **Building materials procurement:** pull bulk per-square-metre or per-kilogram pricing and full technical specifications for tiles, timber, and other quantity-sold materials.
- **Catalog and inventory feeds:** keep an external catalog current with online and in-store availability, seller information, and specifications.
- **Review and reputation research:** collect full review histories with rating breakdowns to analyze product quality and customer sentiment.
- **Recurring change monitoring:** schedule daily or weekly runs to catch new arrivals, price changes, and availability shifts without re-collecting everything each time.

### Data You Get

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

| Field | Example |
| --- | --- |
| `productId` / `sku` | `"12314733"` |
| `name` | `"Sample Cordless Drill Driver, incl. 2 batteries and charger"` |
| `brand` | `"Sample Tools Professional"` |
| `url` | `"https://www.hornbach.de/p/sample-product-name/12314733/"` |
| `price` / `currency` / `priceUnit` | `199.0` / `"€"` / `"ST"` |
| `originalPrice` | `249.0` (manufacturer's suggested price, present when the product is marked down) |
| `discountAmount` / `discountPercent` | `50.0` / `20.1` |
| `packPrice` / `packPriceUnit` | `14.93` / `"m²"` (per-square-metre, per-kilogram, or per-piece pack price, when the site shows one) |
| `rating` / `reviewCount` | `4.9` / `11` |
| `badges` | `["Top item"]` |
| `onlineAvailable` / `onlineAvailabilityText` | `true` / `"Online bestellbar"` |
| `inStock` / `articleNumber` / `imageUrl` / `category` / `scrapedAt` | `true` / `"6324175"` / image URL / `"Bohrmaschinen"` (filled with Fetch details) / run timestamp. Convenience aliases of existing fields; they never trigger an incremental UPDATED. |
| `storePickupAvailable` / `storePickupAvailabilityText` | `true` / `"Reservierbar"` |
| `onlineMerchant` / `storeMerchant` | `{"name": "HORNBACH", "isMarketplaceMerchant": false}` |
| `categoryPath` | `[{"name": "Home", "url": "..."}, {"name": "Machines, Tools and Workshop", "url": "..."}]` (full breadcrumb, with full product detail on, or for a pasted product URL) |
| `ean` | `"4000000000000"` (with full product detail on, or for a pasted product URL) |
| `specifications` | `{"Drive type": "Battery", "Voltage": "18 V", "Weight incl. battery": "2.1 kg"}` (with full product detail on, or for a pasted product URL) |
| `images` | `[{"url": "...", "thumbnailUrl": "...", "alt": "..."}]` (full media gallery, with full product detail on, or for a pasted product URL; otherwise a single `image` / `thumbnailUrl` pair is returned instead) |
| `datasheets` | `[{"title": "Technical data sheet", "url": "..."}]` (with full product detail on, or for a pasted product URL) |
| `storeStockText` / `storeAisleText` | `"20 pcs in stock at the store"` / `"Aisle 12"` (with full product detail on, or for a pasted product URL) |
| `reviews.averageRating` / `reviews.totalReviewCount` | `4.9` / `11` |
| `reviews.ratingDistribution` | `[{"rating": 5, "count": 10}, {"rating": 4, "count": 1}]` (only when reviews are fetched) |
| `reviews.items[]` | `[{"rating": 5, "title": "...", "body": "...", "author": "...", "date": "..."}]` (only when reviews are fetched) |
| `changeType` / `changedFields` | `"NEW"` (also `UPDATED`, `UNCHANGED`, `REAPPEARED`, `EXPIRED`) / `["price"]` on `UPDATED` rows (Incremental mode only) |
| `firstSeenAt` / `lastSeenAt` | `"2026-08-01T00:00:00Z"` / `"2026-08-04T00:00:00Z"` (Incremental mode only) |

### How to Use

1. Pick a **mode**: `search` (keyword and/or category with Hornbach.de's own filters) or `url` (paste product, search, or category links).
2. For search mode, set a keyword (optionally narrowed by a category id) and filter by brand, seller, price, or availability; a category id alone browses the whole department without filters. For url mode, paste the links instead.
3. Turn on **Fetch full product detail** and/or adjust **Fetch reviews** for the depth you need, then set **Max products total** to control run size and cost.
4. Click **Start**, then download the dataset as JSON, CSV, or Excel, or read it through the API.

**Basic keyword search:**

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

**Search narrowed by category, brand, and price:**

```json
{
  "mode": "search",
  "searchTerm": "akkuschrauber",
  "category": "S1937",
  "brands": ["Bosch Professional"],
  "minPrice": 50,
  "maxPrice": 250,
  "sortBy": "PRICE_ASC",
  "maxItems": 30
}
```

**Category browse with full detail and review enrichment:**

```json
{
  "mode": "search",
  "category": "S594",
  "fetchDetails": true,
  "fetchReviews": true,
  "maxReviewsPerProduct": 0,
  "maxItems": 50
}
```

**Paste product, search, or category links:**

```json
{
  "mode": "url",
  "urls": [
    "https://www.hornbach.de/p/sample-product-name/12314733/",
    "https://www.hornbach.de/s/fliesen/",
    "https://www.hornbach.de/c/baustoffe/S594/"
  ],
  "maxItems": 40
}
```

##### Supported URL shapes (URL mode)

| URL shape | Example |
|---|---|
| Keyword search | `https://www.hornbach.de/s/akkuschrauber/` |
| Keyword search, paginated | `https://www.hornbach.de/s/akkuschrauber/?page=2` |
| Keyword search with sort (the link's `sortBy` value is used for that link) | `https://www.hornbach.de/s/akkuschrauber/?page=2&sortBy=PRICE_ASC` |
| Search term with a space | `https://www.hornbach.de/s/bosch%20professional/` |
| Top-level category | `https://www.hornbach.de/c/maschinen-werkzeug-werkstatt/S1937/` |
| Category, paginated | `https://www.hornbach.de/c/maschinen-werkzeug-werkstatt/S1937/?page=2` |
| Nested subcategory | `https://www.hornbach.de/c/maschinen-werkzeug-werkstatt/maschinen/akkuschrauber/S43/` |
| Product page | `https://www.hornbach.de/p/mafell-akku-bohrschrauber-asb-18-pure-im-t-max-akkuschrauber-akkubohrmaschine/12866190/` |
| Product with placeholder slug (site redirects) | `https://www.hornbach.de/p/artikel/6141527/` |
| Product without trailing slash | `https://www.hornbach.de/p/akku-bohrschrauber-gsr-18v-65-bosch-professional-inkl-2-x-akku-4-0-ah-und-ladegeraet-l-case/12314733` |

Any other hornbach.de URL is skipped with a warning in the log. Behaviour note: with the `urls` field left untouched, URL mode now scrapes one example of each shape above (spread over Max products total).

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/hornbach-de-scraper").call(run_input={"mode": "search", "searchTerm": "akkuschrauber", "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/hornbach-de-scraper').call({ mode: 'search', searchTerm: 'akkuschrauber', 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.

#### Resume and recurring updates

- **Resume** (`resumeFromRunId`) continues one specific interrupted or previous run: paste its run id or dataset id and this run skips products already collected there, returning only the remainder. It also protects automatically against a platform migration or restart mid-run, with no input needed.
- **Incremental mode** (`incrementalMode`) is for scheduling the same search again and again, for example daily: the actor remembers the previous run of that same tracked search itself, so no run id needs to be pasted. The first run returns every matching product as `NEW`. A later run compares against that baseline and returns only `NEW`, `UPDATED` and `REAPPEARED` products, with `changedFields` listing what changed; products that have not changed are suppressed and not billed, unless `emitUnchanged` is turned on. Turning on `emitExpired` also returns baseline products that a complete scan of the tracked search no longer found, marked `EXPIRED`; this only fires when the run was not resumed, was not capped by Max products total, and reached the natural end of every search, category, or URL in scope. A product only comes back marked `REAPPEARED` after an earlier complete scan with Emit expired on recorded it as gone (normally output as an `EXPIRED` row) and it then turns up again; a tracked product that was never marked gone that way instead classifies as `UPDATED`, or `UNCHANGED` (suppressed unless Emit unchanged is on), like any other tracked product, never `REAPPEARED`. State is isolated automatically per search/category/URL, filter, and detail/review settings; set `stateKey` to name or deliberately share a monitoring campaign. `UPDATED` is decided by comparing every output field with the previous run (price, original price, discount, badges, availability, store pickup, merchants and so on). The `reviews` block never triggers `UPDATED` or appears in `changedFields` (it depends on the review cap and on whether the review fetch succeeded); it stays in the output. A changed rating or review count does count as a change; add `rating` or `reviewCount` to `ignoreFieldsForChanges` to ignore them. Use `ignoreFieldsForChanges` to exclude more fields (a dotted path such as `onlineMerchant.name` reaches a nested field).
- Pasting `resumeFromRunId` together with `incrementalMode` only bootstraps a brand-new tracked search from an existing dataset. If that state key already has a saved baseline, the run stops with an error instead of mixing the two, so nothing is collected or charged.

#### Output views

The dataset has four views in the Output tab:

- **Products**: one row per product with price, availability, rating and the other top-level fields.
- **Reviews (one row per review)**: the `reviews.items` array unwound so each review is its own row, with the product id and name.
- **Product details**: breadcrumb path, EAN, specifications, images, datasheets and in-store stock text (filled with Fetch full product detail or for pasted product URLs).
- **Changes (incremental mode)**: `changeType`, `changedFields`, `firstSeenAt` and `lastSeenAt` for scheduled runs.

### 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 record, 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 have not authorized a connector yet.
3. For **Notion**, also set `notionParentPageUrl` to the page where items should be created.

The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave the field empty to skip.

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `search` | `search` (keyword/category with filters) or `url` (paste links). |
| `searchTerm` | string | none (prefill: `akkuschrauber`) | Free-text keyword for search mode. URL mode reads the term from the pasted URL instead. |
| `category` | string | none | A Hornbach.de category id, e.g. `S1937`, or a category name such as `Akkuschrauber` (looked up on hornbach.de; the run stops with an error if no category matches). Narrows a keyword search, browses a whole department alone, and also applies to a pasted search-result URL in URL mode; a category or product URL ignores it. |
| `brands` | array | none (prefill: `[]`) | Only these brands. Applies to a keyword search (with or without a category) and to a pasted search-result URL in URL mode. Ignored for a category-only browse (no keyword) and for pasted category or product URLs. |
| `sellers` | array | none (prefill: `[]`) | Only these named sellers. Same scope as `brands`. |
| `minPrice` | integer | none | Minimum price in EUR. Same scope as `brands`. |
| `maxPrice` | integer | none | Maximum price in EUR. Same scope as `brands`. |
| `availability` | string | \`\` (any) | `IN_STOCK`, `ONLINE`, `RESERVABLE`, or `NEW`. Same scope as `brands`. |
| `sortBy` | string | `SCORE` | `SCORE`, `PRICE_ASC`, `PRICE_DESC`, `RATING_DESC`, `BRAND_ASC`, `BRAND_DESC`, `TITLE_ASC`, `TITLE_DESC`. Same scope as `brands`; a `sortBy` value inside a pasted search link takes precedence over this field for that link. |
| `urls` | array | one example of each supported URL shape (see the URL table above) | Product, search, or category links for URL mode. Leaving the field untouched scrapes one example of every shape; clear it and paste your own links to scrape only those. |
| `fetchDetails` | boolean | `false` | Adds the full breadcrumb path, description, flattened specifications, EAN, media gallery, variant matrix, and exact in-store stock text. Keyword and search-URL results already include current price, was-price/discount, pack price, and rating without this toggle; category-browse results (category id without a keyword, or a pasted category URL) include only price, rating, and basic availability unless this toggle is on. A pasted product URL is always scraped from its full product page, so it always returns these detail fields and always counts as enriched, even with this toggle off. |
| `fetchReviews` | boolean | `true` | Fetches each product's full review history. Off leaves a rating/count summary with no individual reviews. |
| `maxReviewsPerProduct` | integer | `20` | Cap on reviews fetched per product when Fetch reviews is on; `0` = all available, up to 1,000 per product (values above 1,000 are treated as 1,000). |
| `maxPages` | integer | `0` | Cap on result pages per search/category/URL entry; `0` = unlimited, walk every page until the site's own last page or Max products total is hit. |
| `maxItems` | integer | `20` | Hard cap on total products returned across every entry; `0` = unlimited. |
| `searchQuery` | string | none | Alias of Search keyword. The primary name wins when both are set. |
| `categoryUrl` | string | none | Alias for pasting a category or search page URL: switches the run to URL mode and scrapes it ahead of any URLs you pasted yourself (the untouched example URL list is replaced). Ignored when `category` is set. |
| `maxResults` | integer | none | Alias of Max products total. Takes effect while `maxItems` is left at its default of 20; a `maxItems` you changed wins. |
| `resumeFromRunId` | string | none | Continue one specific previous run or dataset: already-collected products are skipped. For recurring monitoring of the same search, use Incremental mode instead. |
| `incrementalMode` | boolean | `false` | Track this same search/category/URL set across scheduled runs and return only what changed. See "Resume and recurring updates" above. |
| `stateKey` | string | none | Name a monitoring campaign, or deliberately share state across differently-configured runs. Left empty, a key is derived automatically from the search, filters, and detail/review settings. |
| `emitUnchanged` | boolean | `false` | Incremental mode only. Also return (and bill) products unchanged since the last run, marked `UNCHANGED`. |
| `emitExpired` | boolean | `false` | Incremental mode only. Also return (and bill) products from the baseline no longer found, marked `EXPIRED`, once a run completes a full scan of the tracked search. |
| `ignoreFieldsForChanges` | array | none (prefill: `[]`) | Incremental mode only. Extra field names (or dotted paths for nested fields) that never count as a change; they still appear in every row. |
| `proxy` | object | `{"useApifyProxy": true}` | Connection configuration. |
| `mcpConnectors` | array | none | Optional MCP connectors to also receive a summary of results. |
| `notionParentPageUrl` | string | none | Required only when a Notion connector is selected. |
| `maxNotifyListings` | integer | `50` | Cap on items sent to each connector; does not affect the dataset. |

### Output Example

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

```json
{
  "productId": "12314733",
  "sku": "12314733",
  "concreteProductId": "12314733_ST",
  "name": "Sample Cordless Drill Driver, incl. 2 batteries and charger",
  "url": "https://www.hornbach.de/p/sample-product-name/12314733/",
  "brand": "Sample Tools Professional",
  "price": 199.0,
  "currency": "€",
  "priceUnit": "ST",
  "originalPrice": null,
  "discountAmount": null,
  "discountPercent": null,
  "packPrice": null,
  "packPriceUnit": null,
  "badges": ["Top item"],
  "rating": 4.9,
  "reviewCount": 11,
  "onlineAvailable": true,
  "onlineAvailabilityText": "Online bestellbar",
  "storePickupAvailable": true,
  "storePickupAvailabilityText": "Reservierbar",
  "onlineMerchant": { "name": "HORNBACH", "isMarketplaceMerchant": false },
  "storeMerchant": { "name": "HORNBACH", "isMarketplaceMerchant": false },
  "categoryPath": [
    { "name": "Home", "url": "https://www.hornbach.de/" },
    { "name": "Machines, Tools and Workshop", "url": "https://www.hornbach.de/c/sample/S1937/" }
  ],
  "ean": "4000000000000",
  "specifications": {
    "Drive type": "Battery",
    "Voltage": "18 V",
    "Weight incl. battery": "2.1 kg"
  },
  "images": [
    { "url": "https://media.hornbach.de/hb/packshot/sample.jpg", "thumbnailUrl": "https://media.hornbach.de/hb/packshot/sample_thumb.jpg", "alt": "Sample product image" }
  ],
  "datasheets": [
    { "title": "Technical data sheet", "url": "https://media.hornbach.de/sample.pdf" }
  ],
  "reviews": {
    "averageRating": 4.9,
    "totalReviewCount": 11,
    "ratingDistribution": [
      { "rating": 5, "count": 10 },
      { "rating": 4, "count": 1 }
    ],
    "items": [
      {
        "rating": 5,
        "title": "Sample review title",
        "body": "Sample review text describing the product experience.",
        "author": "Sample User",
        "date": "2026-05-01T00:00:00.000Z",
        "isRecommended": true,
        "secondaryRatings": { "Value": 5, "Quality": 5 }
      }
    ]
  }
}
```

### Plan Requirement

Hornbach.de puts a JavaScript check in front of its pages, so plain HTTP requests are blocked even from a datacenter connection. The scraper clears that check once in a browser, which works on the default Apify datacenter connection: no residential proxy is required and the default setting is enough for typical runs. A residential connection is optional and can give more headroom for very large or sustained monitoring. Apify free plans do not include any Apify proxy, so a paid plan is needed for the default connection, or supply your own proxy URLs in the Proxy field.

Connection messages you may see in the run log: "The selected connection is unavailable to this run. Apify free plans do not include residential proxies. Continuing on a backup connection; some pages may still be unavailable." and "Served over the backup connection (the default connection was unavailable)." These are warnings; the run continues.

### FAQ

#### How much does it cost?

You pay per product returned (the result event), a small one-time start charge per run, and a `detail-enrichment` charge of one unit per product only when full product detail and/or reviews are fetched for that product. It is charged right after that product is delivered, once per product (never per review, and never twice when both detail and reviews are on), and never for unchanged products suppressed by incremental mode, duplicates, or failed products (a product link that could not be loaded produces no row at all). The **Pricing** tab shows the current rates. Use **Max products total** to cap the cost of any run.

#### Is it legal to scrape Hornbach.de?

This actor collects only publicly available product data. You are responsible for how you use it: follow Hornbach.de's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Product names, prices, and specifications are generally facts, but images and review text may be subject to third-party rights.

#### Can I get only new or changed products on a schedule?

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental mode**. Each later run then returns only new, updated and reappeared products for that tracked search, and unchanged ones are suppressed and not billed unless you turn **Emit unchanged** on.

#### What is the difference between Resume and Incremental mode?

Resume (`resumeFromRunId`) continues ONE specific interrupted or previous run: paste its run or dataset id and only the remaining new products are returned. Incremental mode is for running the SAME search again and again on a schedule; the actor remembers the previous run of that search by itself, with no id to paste. Combining the two only bootstraps a brand-new tracked search from an existing dataset; if that search already has saved state, the run stops with an error instead of mixing the two.

#### When does a product come back marked REAPPEARED?

Only after an earlier complete scan with **Emit expired** on recorded that same product as gone (normally output as an `EXPIRED` row), which itself only happens when Emit expired was on and that earlier run completed a full, uncapped scan of the tracked search. A tracked product that was never marked gone that way instead classifies as `UPDATED`, or `UNCHANGED` (suppressed unless Emit unchanged is on), like any other tracked product, never `REAPPEARED`.

#### Why might a run fail, or come back empty, instead of returning products?

A run fails with a clear message in three cases (the connection message only when a page was really requested and never answered; a query the site answered with an error is reported as a rejected query instead). (1) An invalid Resume id, or Resume combined with Incremental mode on a search that already has saved state: both stop before anything is collected or charged. (2) No page could be read at all. If the proxy connection was refused for the account: "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." If residential was refused and the backup connection could not load pages: "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 Hornbach pages. Upgrade at https://apify.com/pricing or supply your own proxy URLs, then try again." For any other failure to load a results 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." (3) The input leaves nothing to scan, and the run fails before any page is requested with an input message instead of the connection one: "Search mode needs a search keyword and/or a category. Enter one of them and run again.", "URL mode needs at least one pasted URL. Add a HORNBACH product, search or category URL and run again.", "None of the pasted URLs is a supported HORNBACH product (/p/), search (/s/) or category (/c/) URL, so nothing was scanned. Check the URLs and run again.", or, for a category name that matches no category, "Category '<name>' is not a category id (e.g. S1937) and no HORNBACH category matches that name. Use an id from a hornbach.de/c/<name>/S<id>/ URL." If HORNBACH answers the search (or the category-name lookup) with an error instead of results, the run fails with "HORNBACH rejected the search query (check the category id, brand, seller and price filters), so nothing was collected." A pasted product link that no longer exists (HTTP 404/410) or has no product on the page is skipped with a warning: no row is written, nothing is billed, and no re-authentication is attempted for it. A category name is looked up once, in both modes. A genuine search with no matches finishes normally with zero products.

#### 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 e-commerce data?

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

<table>
<tr><td>🛒 <a href="https://apify.com/abotapi/obi-de-scraper"><b>OBI.de Scraper</b></a><br>Scrape OBI (obi.de) DIY/home-improvement products: current + strike-through original...</td><td>🛒 <a href="https://apify.com/abotapi/rossmann-de-scraper"><b>ROSSMANN.de Scraper</b></a><br>Scrape ROSSMANN (rossmann.de) drugstore products: current + original strike-through price...</td></tr>
<tr><td>🛒 <a href="https://apify.com/abotapi/dm-de-scraper"><b>dm.de</b></a><br>Scrape dm-drogerie markt (dm.de) products: current + strike-through Ausverkauf price with...</td><td>🥦 <a href="https://apify.com/abotapi/kaufland-de-scraper"><b>Kaufland.de Scraper</b></a><br>Scrape Kaufland.de, Germany's hypermarket and online marketplace: keyword/category/brand...</td></tr>
<tr><td>🛒 <a href="https://apify.com/abotapi/thalia-de-scraper"><b>Thalia.de Scraper</b></a><br>Scrape Thalia.de books, eBooks, audiobooks, toys and stationery. Search by keyword or...</td><td>🛒 <a href="https://apify.com/abotapi/idealo-de-price-comparison-scraper"><b>Idealo.de</b></a><br>Scrape Idealo.de products with identity details, GTIN, price ranges, specifications...</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/hornbach-de-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 category with HORNBACH's own filters. 'url' scrapes any product, search-result, or category page URL you paste, walking pagination forward automatically.

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

Free-text keyword, e.g. 'akkuschrauber' or 'fliesen'. Combine with Category below to narrow a keyword search, or leave empty and set only Category to browse a whole department.

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

Alias of Search keyword, accepted for compatibility with other HORNBACH scrapers. Ignored when Search keyword is set.

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

A HORNBACH category id, e.g. 'S1937' (Maschinen, Werkzeug & Werkstatt), or a category name such as 'Akkuschrauber' which is looked up on hornbach.de; the run stops with an error if no category matches the name. Ids are in category page URLs: hornbach.de/c/<name>/S<id>/. Narrows a keyword search, or browses the whole department when Search keyword is left empty. In URL mode it also narrows pasted search-result links; category and product links ignore it.

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

Alias: a hornbach.de category or search page URL. Switches the run to URL mode and scrapes it ahead of any Start URLs you pasted yourself (the untouched example list is replaced). Ignored when Category is set.

## `brands` (type: `array`):

Only return products from these brands, e.g. 'Bosch Professional', 'Makita', 'Einhell'. Match the exact brand name as shown on the site. Leave empty for all brands.

## `sellers` (type: `array`):

Only return offers from these named sellers (HORNBACH itself plus third-party marketplace sellers), e.g. 'Rubart GmbH'. Leave empty for all sellers.

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

Only return products priced at or above this amount.

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

Only return products priced at or below this amount.

## `availability` (type: `string`):

Narrow by stock/availability, or by new-in-range products.

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

Order of the returned results.

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

Paste one or more HORNBACH product pages (/p/.../<id>/), search-result pages (/s/<term>/), or category pages (/c/.../S<id>/). Pagination continues forward automatically from any page number already in the URL. The filters and sort order above apply to pasted search-result links (a sortBy value in the link itself wins over the Sort order field); category and product links are scraped as-is. Left untouched, URL mode scrapes one example of each supported URL shape (search, paginated search, sorted search, category, nested category, product).

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

Adds the full breadcrumb category path, complete description, flattened technical specifications, EAN/GTIN, media gallery, variant matrix, safety/legal hints, and exact in-store stock text. Current price, was-price/discount, per-quantity price (e.g. per square metre) and rating are already included without this toggle.

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

Fetch each product's full review history: overall rating, review count, rating breakdown, and per-review author/date/rating/title/body. Products with no reviews simply return an empty list.

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

Cap on reviews fetched per product when 'Fetch reviews' is on. 0 = all available reviews, up to 1,000 per product.

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

Stop after this many result pages per search/category/URL entry. 0 (default) = unlimited, walk every page until the site's own last page or Max products total is hit.

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

Hard cap on total products returned across every search/category/URL entry. 0 = unlimited (still bounded by Max pages per search).

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

Alias of Max products total, accepted for compatibility. Takes effect while Max products total is left at its default of 20; a Max products total you changed wins.

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

The default connection works on every Apify plan, including the free tier. A residential connection is optional and only needed for very large or sustained runs.

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

Paste a previous run id (or its dataset id) to continue that run: products already collected there are skipped, so this run only returns new products (a delta). Use this after an interrupted run, or to continue a large catalogue pull across multiple runs. For recurring daily/weekly monitoring of the SAME search, use Incremental mode below instead. Leave empty for a normal run.

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

Turn this on for daily or recurring monitoring of the same search/category/URL set. The first run returns all matching products as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED products. UPDATED is decided by comparing every output field with the previous run (price, original price, discount, badges, availability and store pickup, merchant and so on); changedFields lists which ones moved. The reviews block never triggers UPDATED; a changed rating or review count does, unless you list them in Ignore fields below. Turn on "Emit unchanged" or "Emit expired" below only when you also want those products returned (and billed). State is kept separately for each search/category/URL and detail/review-mode setup; use State key to name or deliberately share a monitoring campaign. To continue one specific interrupted run instead, use Resume from run/dataset id 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/category/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.

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

Optional. Extra field names to leave out when deciding whether a product changed. Fields listed here still appear in every row; they just never trigger UPDATED or show up in changedFields. Use a dotted path for a nested field, e.g. onlineMerchant.name. The reviews block is always left out (it depends on the review cap and on whether the review fetch succeeded); rating and reviewCount do count as changes unless you list them here.

## `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": "akkuschrauber",
  "brands": [],
  "sellers": [],
  "availability": "",
  "sortBy": "SCORE",
  "urls": [
    "https://www.hornbach.de/s/akkuschrauber/",
    "https://www.hornbach.de/s/akkuschrauber/?page=2",
    "https://www.hornbach.de/s/akkuschrauber/?page=2&sortBy=PRICE_ASC",
    "https://www.hornbach.de/s/bosch%20professional/",
    "https://www.hornbach.de/c/maschinen-werkzeug-werkstatt/S1937/",
    "https://www.hornbach.de/c/maschinen-werkzeug-werkstatt/S1937/?page=2",
    "https://www.hornbach.de/c/maschinen-werkzeug-werkstatt/maschinen/akkuschrauber/S43/",
    "https://www.hornbach.de/p/mafell-akku-bohrschrauber-asb-18-pure-im-t-max-akkuschrauber-akkubohrmaschine/12866190/",
    "https://www.hornbach.de/p/artikel/6141527/",
    "https://www.hornbach.de/p/akku-bohrschrauber-gsr-18v-65-bosch-professional-inkl-2-x-akku-4-0-ah-und-ladegeraet-l-case/12314733"
  ],
  "fetchDetails": false,
  "fetchReviews": true,
  "maxReviewsPerProduct": 20,
  "maxPages": 0,
  "maxItems": 20,
  "proxy": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 50
}
```

# Actor output Schema

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

No description

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

No description

## `detail` (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": "akkuschrauber",
    "brands": [],
    "sellers": [],
    "urls": [
        "https://www.hornbach.de/s/akkuschrauber/",
        "https://www.hornbach.de/s/akkuschrauber/?page=2",
        "https://www.hornbach.de/s/akkuschrauber/?page=2&sortBy=PRICE_ASC",
        "https://www.hornbach.de/s/bosch%20professional/",
        "https://www.hornbach.de/c/maschinen-werkzeug-werkstatt/S1937/",
        "https://www.hornbach.de/c/maschinen-werkzeug-werkstatt/S1937/?page=2",
        "https://www.hornbach.de/c/maschinen-werkzeug-werkstatt/maschinen/akkuschrauber/S43/",
        "https://www.hornbach.de/p/mafell-akku-bohrschrauber-asb-18-pure-im-t-max-akkuschrauber-akkubohrmaschine/12866190/",
        "https://www.hornbach.de/p/artikel/6141527/",
        "https://www.hornbach.de/p/akku-bohrschrauber-gsr-18v-65-bosch-professional-inkl-2-x-akku-4-0-ah-und-ladegeraet-l-case/12314733"
    ],
    "proxy": {
        "useApifyProxy": true
    },
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "ignoreFieldsForChanges": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/hornbach-de-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": "akkuschrauber",
    "brands": [],
    "sellers": [],
    "urls": [
        "https://www.hornbach.de/s/akkuschrauber/",
        "https://www.hornbach.de/s/akkuschrauber/?page=2",
        "https://www.hornbach.de/s/akkuschrauber/?page=2&sortBy=PRICE_ASC",
        "https://www.hornbach.de/s/bosch%20professional/",
        "https://www.hornbach.de/c/maschinen-werkzeug-werkstatt/S1937/",
        "https://www.hornbach.de/c/maschinen-werkzeug-werkstatt/S1937/?page=2",
        "https://www.hornbach.de/c/maschinen-werkzeug-werkstatt/maschinen/akkuschrauber/S43/",
        "https://www.hornbach.de/p/mafell-akku-bohrschrauber-asb-18-pure-im-t-max-akkuschrauber-akkubohrmaschine/12866190/",
        "https://www.hornbach.de/p/artikel/6141527/",
        "https://www.hornbach.de/p/akku-bohrschrauber-gsr-18v-65-bosch-professional-inkl-2-x-akku-4-0-ah-und-ladegeraet-l-case/12314733",
    ],
    "proxy": { "useApifyProxy": True },
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "ignoreFieldsForChanges": [],
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/hornbach-de-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": "akkuschrauber",
  "brands": [],
  "sellers": [],
  "urls": [
    "https://www.hornbach.de/s/akkuschrauber/",
    "https://www.hornbach.de/s/akkuschrauber/?page=2",
    "https://www.hornbach.de/s/akkuschrauber/?page=2&sortBy=PRICE_ASC",
    "https://www.hornbach.de/s/bosch%20professional/",
    "https://www.hornbach.de/c/maschinen-werkzeug-werkstatt/S1937/",
    "https://www.hornbach.de/c/maschinen-werkzeug-werkstatt/S1937/?page=2",
    "https://www.hornbach.de/c/maschinen-werkzeug-werkstatt/maschinen/akkuschrauber/S43/",
    "https://www.hornbach.de/p/mafell-akku-bohrschrauber-asb-18-pure-im-t-max-akkuschrauber-akkubohrmaschine/12866190/",
    "https://www.hornbach.de/p/artikel/6141527/",
    "https://www.hornbach.de/p/akku-bohrschrauber-gsr-18v-65-bosch-professional-inkl-2-x-akku-4-0-ah-und-ladegeraet-l-case/12314733"
  ],
  "proxy": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": []
}' |
apify call abotapi/hornbach-de-scraper --silent --output-dataset

```

## MCP server setup

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