# OTTO.de Scraper - Products, Prices, Variants & Reviews (`abotapi/otto-de-scraper`) Actor

Scrape OTTO.de products by search or URL. Extract prices, discounts, variants, availability, brand, seller, EAN/GTIN, specifications, categories, and customer reviews across fashion, furniture, electronics, and homeware.

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

## OTTO Scraper: Prices, Discounts, Variants and Reviews

OTTO Scraper turns OTTO into structured product data. Search by keyword or category, or paste product and listing links directly, and get the current price, the strike-through original price with the site's discount percent when a product is marked down, colour variants with their own variant id and colour code, brand, seller identity when detail is fetched, and customer reviews for fashion, furniture, technik and household appliances. Export the results to JSON, CSV or Excel, or pull them straight into your app through the API.

### Why This Scraper?

- **Price and discount, already structured.** When a product is marked down, the current price, the original price, the site's discount percent and its own promo label all come from one listing fetch, at no extra cost.
- **Colour variant matrix.** Every colour option a product offers is returned with its own variant id and colour code.
- **Specials, first class.** `specialsOnly` narrows any search to only products currently showing a reduced price, and `sortBy: DISCOUNT` surfaces the biggest reductions first, both driven by the site's own live discount signal.
- **Seller transparency.** With Fetch full product detail on, the seller is captured, distinguishing OTTO itself from third-party marketplace partners, with the partner's own identifier.
- **Native review history.** Overall rating, review count, and individual reviews (up to Max reviews per product, 10 by default, or every review the page renders with it set to 0) with author, location, date, star rating, verified-purchase flag, body text and helpful-vote counts, sourced directly from the product page.
- **Rich detail on demand.** Full breadcrumb category path, flattened technical specifications (these vary per product category), EAN/GTIN/SKU, the full image gallery and the return policy.
- **Built for schedules.** Incremental mode returns only new and changed products on recurring runs, Resume continues one interrupted run without double charges, and a long run survives an Apify platform interruption through its own checkpoint.

### Use Cases

- **Deal hunters and price trackers:** monitor a category or brand for the current price, the original price and the discount percent so you can time a purchase or a repricing decision.
- **Marketplace and assortment research:** compare OTTO's own listings against third-party marketplace partners selling the same or similar products, and see who actually fulfils each one.
- **Review and reputation analysis:** build a dataset of native customer reviews per product or category for sentiment, quality, or complaint analysis.
- **Catalogue monitoring:** track new, updated and removed products in a search or category on a recurring schedule.
- **Product data enrichment:** pull structured specs, EAN/GTIN/SKU, images and return policy into your own catalogue or PIM system.

### Data You Get

| Field | Example |
| --- | --- |
| `productId` / `variationId` / `articleNumber` | `"S0AB12CD"` / `"S0AB12CD4XYZ"` / `"S0AB12CDKIE"` |
| `name` | `"Sample Brand Ecksofa, Sofa L-Form, mit Schlaffunktion"` |
| `brand` | `"Sample Brand"` |
| `category` / `breadcrumb` | `"Ecksofas"` / `[{"name": "Möbel", "url": "..."}, {"name": "Sofas", "url": "..."}, {"name": "Ecksofas", "url": "..."}]` |
| `url` | `"https://www.otto.de/p/sample-ecksofa-S0AB12CD/"` |
| `price` / `currency` | `799.00` / `"EUR"` |
| `originalPrice` / `discountPercent` / `promoLabel` | `1599.00` / `50` / `"-50%"` |
| `isOnSpecial` | `true` |
| `availability` / `availabilityText` | `"AVAILABLE"` / `"lieferbar in 5 Wochen"` |
| `rating` / `reviewCount` | `4.5` / `26` |
| `link` / `priceCurrency` / `aggregateRating` | Aliases of `url` / `currency` / `{ratingValue, reviewCount}`, for drop-in use with other OTTO scrapers |
| `variants[]` | `[{"variationId": "S0AB12CD4XYZ", "color": "Khaki", "hexCode": "BEB2A9", "selected": true}]` |
| `image` / `images[]` | `"https://sample.example/img1.jpg"` / `["https://sample.example/img1.jpg", "https://sample.example/img2.jpg"]` |
| `ean` / `gtin` / `sku` | `"4068181820000"` / `"4068181820000"` / `"S0AB12CD4XYZ"` |
| `specs` | `{"Höhe": "85 cm", "Breite": "289 cm", "Polsterung": "Schaumstoff", "Design": "Modern"}` (flattened, fields vary per category) |
| `seller` | `{"name": "Sample Home Brands GmbH", "partnerId": "1000000", "sellerType": "MARKETPLACE_PARTNER"}` |
| `returnPolicy` | `{"days": 30, "freeReturn": true}` |
| `reviews.averageRating` / `reviews.reviewCount` | `4.5` / `26` |
| `reviews.items[]` | `[{"reviewId": "sample-uuid", "author": "Sample R.", "location": "Sample City", "date": "11.04.2026", "rating": 5, "verifiedPurchase": true, "title": null, "body": "Sample review text.", "helpfulVotes": 5, "helpfulVotesTotal": 5, "variantPurchased": "Farbe: Khaki", "seller": "Sample Home Brands GmbH"}]` |
| `changeType` / `changedFields` / `firstSeenAt` / `lastSeenAt` | `"UPDATED"` / `["price"]` / `"2026-08-01T00:00:00Z"` / `"2026-08-17T00:00:00Z"` (incremental mode only) |

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

`category`, `breadcrumb`, `specs`, `seller`, `ean`, `gtin`, `sku` and `returnPolicy` are only populated when full product detail is fetched, either because you turned on **Fetch full product detail**, or because the record came from a pasted product URL, which always fetches its own detail page.

### How to Use

1. Pick a **mode**: `search` (keyword or category name) or `url` (paste product, search or category links).
2. Fill in the field for that mode, and switch on the narrowing filters or detail toggles you need.
3. Set **Max products total** to control run size and cost, then click **Start**.
4. Download the dataset as JSON, CSV or Excel, or read it through the API.

**Keyword search, first page, with reviews (the defaults):**

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

**Category-style search, filtered to specials and sorted by biggest discount:**

```json
{
  "mode": "search",
  "searchTerm": "sofas",
  "specialsOnly": true,
  "sortBy": "DISCOUNT",
  "maxItems": 50,
  "maxPages": 5
}
```

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

```json
{
  "mode": "url",
  "urls": ["https://www.otto.de/p/sample-ecksofa-S0AB12CD/"],
  "fetchDetails": true,
  "fetchReviews": true,
  "maxReviewsPerProduct": 0
}
```

**Paste a listing link and keep paginating forward:**

```json
{
  "mode": "url",
  "urls": ["https://www.otto.de/suche/ecksofa/"],
  "maxItems": 100,
  "maxPages": 10
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

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

#### Full-catalogue walk and Max products

**Max pages per search** (`maxPages`) is `0` (no explicit limit, hard-capped at 1000 pages per entry) by default: a run walks every result page for a search, category or pasted listing URL until the site's own last page, **Max products total** (`maxItems`), or a page that adds no new products, whichever comes first. Set `maxPages` to a positive number only if you want to cap the number of page fetches directly.

#### Resume and recurring updates

- **Resume** (`resumeFromRunId`) continues one interrupted run: paste its run or dataset id and the actor skips every product already collected there, so this run only returns new products, a delta, instead of a full re-scrape.
- **Incremental mode** (`incrementalMode`) is for scheduled runs over the same search or URL set. Each product is classified `NEW`, `UPDATED` (with `changedFields`), or `UNCHANGED` (suppressed and not billed unless `emitUnchanged` is on). `EXPIRED` detection only runs at all when `emitExpired` is on, and only produces rows on a run that fully scanned the tracked search or URL set: not capped by `maxItems` or `maxPages`, not using `resumeFromRunId`, no failed page along the way, no pasted URL that starts at an offset (`&o=`) or carries a `preis=` price range (those are partial scopes), and at least one product scanned; turn on `emitExpired` to have those rows returned and billed. `REAPPEARED` only appears for a product that an earlier complete-scan run with `emitExpired` on had already marked `EXPIRED` and that has now been found again. `stateKey` names or shares the stored state; leave it empty and the actor derives one automatically from the search and detail settings. What is compared: every returned field of the product, including nested variants and reviews; an integral price (25 and 25.0) is one value. Upgrade note: a product tracked before this release is not re-reported as `UPDATED` just because it now carries the `link`, `priceCurrency` and `aggregateRating` aliases, anonymous reviews now have their location and date filled in, or `maxReviewsPerProduct` above 12 now appends more reviews after the ones already stored; a genuine review edit, a removed or reordered review, or any other change still counts as `UPDATED`. Nothing is excluded by default. `ignoreFieldsForChanges` lists top-level field names to leave out of change detection and `changedFields`; they stay in the output, and editing the list never causes a one-time wave of UPDATED rows. With incremental mode off, output is exactly as before.
- A long run also checkpoints its own progress: if it is interrupted by an Apify platform migration, or resumed with the **Resurrect** button, it picks up where it left off with no duplicate products and no double charges.

#### URL mode: supported URL shapes

| Shape | Example | Notes |
| --- | --- | --- |
| Keyword search | `https://www.otto.de/suche/ecksofa/` | Walks all pages. |
| Category (nested) | `https://www.otto.de/moebel/sofas/ecksofas/` | |
| Category (parent) | `https://www.otto.de/moebel/sofas/` | |
| Category (other department) | `https://www.otto.de/technik/fernseher/` | The site's internal taxonomy differs from the path; resolved from the page itself. |
| Search with `suchbegriff` | `https://www.otto.de/suche/laptop/?suchbegriff=laptop` | |
| Department hub | `https://www.otto.de/moebel/` | Top-level department page. Probed live: it embeds the same product listing route as a category and returns products (120 per page), so it is scraped like a category. |
| Sorted search / category | `.../suche/ecksofa/?sortiertnach=preis-aufsteigend` | The pasted sort is used unless you set **Sort by** explicitly. |
| Filtered category | `.../moebel/sofas/ecksofas/?marke=sit-more&preis=100-500` | `marke` is applied by the site; the price range is applied by the actor, because the site drops it. |
| Listing JSON route | `https://www.otto.de/dundee/tilelist?rule=...&sortiertnach=topseller` | Pasted as-is. `&o=120` starts at that offset: a **partial scope** (a warning is logged). |
| Apex host | `https://otto.de/suche/ecksofa` | Normalised to `www.otto.de`. |
| Product page | `https://www.otto.de/p/<slug>-<id>/` | Fetches full detail; accepted but not part of the default examples. |

`/shoppages/` and `/marken/` pages are not product listings: they are skipped with a warning per URL.

**Behaviour change:** with the URL field left untouched (or omitted), url mode now scrapes one example of each listing shape above instead of one search page. Pass your own `urls` to control this; `maxItems` (default 20) is shared across them.

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

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

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

1. Authorize a connector once under **Apify, Settings, Integrations** (Notion, Linear, Airtable, or Apify).
2. Select it in the **"Pipe results into your apps"** input field. If the picker is empty, you 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 own 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 or category) or `url` (paste product, search, or category links). |
| `searchTerm` | string | none (prefill: `"ecksofa"`) | Keyword or category name to search (search mode only). |
| `urls` | array | one example per supported URL shape (see "URL mode") | Product, search, or category page URLs to scrape (url mode only). Pagination continues forward automatically. `startUrls` is accepted as an alias and merged in. |
| `specialsOnly` | boolean | `false` | Keep only currently-discounted products. Applies to a keyword search and a pasted search or category URL. Also evaluated on a single pasted product URL, but the detail page never exposes special-offer status, so it always excludes a pasted product link, on special or not; leave it off when pasting a single product URL. |
| `brands` | array | none (prefill: `[]`) | Exact brand names to keep. Applies to a keyword search, a pasted search or category URL, and a single pasted product URL. Leave empty for all brands. |
| `minRating` | integer | none | 1 to 5, minimum rating to keep. Applies to a keyword search, a pasted search or category URL, and a single pasted product URL. |
| `minPrice` | number | none | Minimum price in EUR to keep. Applies to a keyword search, a pasted search or category URL, and a single pasted product URL. |
| `maxPrice` | number | none | Maximum price in EUR to keep. Applies to a keyword search, a pasted search or category URL, and a single pasted product URL. |
| `sortBy` | string | `RELEVANCE` | `RELEVANCE`, `PRICE_ASC`, `PRICE_DESC`, `DISCOUNT`, `NEWEST`, or `RATING`. Applies to a keyword search and a pasted search or category URL; has no effect on a single pasted product URL, since there is nothing to reorder. |
| `fetchDetails` | boolean | `false` | Fetch full product detail for a product found through search or a listing: breadcrumb, flattened specs, seller identity, EAN/GTIN/SKU, return policy and full image gallery. A pasted product URL always includes these fields regardless of this toggle. |
| `fetchReviews` | boolean | `true` | Fetch native customer reviews. Products with no reviews return an empty list. |
| `maxReviewsPerProduct` | integer | `10` | Cap on reviews fetched per product when `fetchReviews` is on. `0` returns all the reviews the product page renders. Values above `1000` are capped at `1000`. The product page renders about a dozen reviews; when you ask for more (and the product has more), the actor reads the rest from OTTO's own review list, 30 per page, which uses the same residential connection and adds no extra charge beyond the once-per-product detail enrichment. `0` stays at what the product page renders. |
| `maxPages` | integer | `0` | Stop after this many result-page fetches per search, category or URL entry. `0` means no explicit limit: the run walks the catalogue and stops at Max products total, with an internal hard cap of 1000 pages per entry. |
| `maxItems` | integer | `20` | Hard cap on total products returned across every search, category, or URL entry. `0` means the maximum of 100000 (still bounded by Max pages per search). |
| `resumeFromRunId` | string | none | 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. Leave empty for a normal run. |
| `incrementalMode` | boolean | `false` | Turn on for recurring monitoring of the same search or URL set. See "Resume and recurring updates" above for exactly how NEW, UPDATED, UNCHANGED, REAPPEARED and EXPIRED are decided. |
| `stateKey` | string | none | Optional, incremental mode only. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently-configured runs. Leave empty to derive a key automatically. |
| `emitUnchanged` | boolean | `false` | Incremental mode only. Also return, and bill, products that have not changed since the last run. |
| `emitExpired` | boolean | `false` | Incremental mode only. Also return, and bill, products that were present in a previous run but are no longer found. Only produced once a run has fully scanned the tracked search or URL set. |
| `ignoreFieldsForChanges` | array of strings | none | Incremental mode only. Top-level field names to exclude from UPDATED detection and `changedFields` (still returned in the output). |
| `proxy` | object | Apify Proxy | Connection settings for the product listing and search fetch. Product detail and review fetches (on by default) always use a residential connection at extra cost, regardless of this setting. |
| `mcpConnectors` | array | none | Optional: send a summary of each product to apps you authorized under Integrations. |
| `notionParentPageUrl` | string | none | Notion connector only: page under which item pages are created. |
| `maxNotifyListings` | integer | `50` | Cap on items exported to each connector per run. Does not affect the dataset. |

### Output Example

```json
{
  "productId": "S0AB12CD",
  "variationId": "S0AB12CD4XYZ",
  "articleNumber": "S0AB12CDKIE",
  "name": "Sample Brand Ecksofa, Sofa L-Form, mit Schlaffunktion",
  "brand": "Sample Brand",
  "category": "Ecksofas",
  "url": "https://www.otto.de/p/sample-ecksofa-S0AB12CD/",
  "price": 799.00,
  "currency": "EUR",
  "originalPrice": 1599.00,
  "discountPercent": 50,
  "isOnSpecial": true,
  "availability": "AVAILABLE",
  "rating": 4.5,
  "reviewCount": 26,
  "variants": [
    { "variationId": "S0AB12CD4XYZ", "color": "Khaki", "hexCode": "BEB2A9", "selected": true }
  ],
  "image": "https://sample.example/img1.jpg",
  "specs": { "Höhe": "85 cm", "Breite": "289 cm", "Polsterung": "Schaumstoff" },
  "seller": { "name": "Sample Home Brands GmbH", "partnerId": "1000000", "sellerType": "MARKETPLACE_PARTNER" },
  "returnPolicy": { "days": 30, "freeReturn": true },
  "reviews": {
    "averageRating": 4.5,
    "reviewCount": 26,
    "items": [
      {
        "reviewId": "sample-uuid",
        "author": "Sample R.",
        "location": "Sample City",
        "date": "11.04.2026",
        "rating": 5,
        "verifiedPurchase": true,
        "body": "Sample review text.",
        "helpfulVotes": 5,
        "helpfulVotesTotal": 5,
        "variantPurchased": "Farbe: Khaki",
        "seller": "Sample Home Brands GmbH"
      }
    ]
  }
}
```

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

### Plan Requirement

Searching and browsing listings (search, category and listing-route links) works on the default connection, including Apify datacenter proxies, on every plan. Product detail pages and reviews (reviews are on by default) are different: the product page answers a plain request with an anti-bot interstitial, so detail and reviews always use a residential German connection at extra cost, regardless of the connection setting you choose. Residential access needs a paid Apify plan or your own proxy URLs.

What you see when residential access is not available:

- If the selected connection is unavailable, the run logs "The selected connection is unavailable to this run. Apify free plans do not include residential proxies." and "Continuing on a backup connection; some pages may still be unavailable." Pages served that way log "Served over the backup connection (the default connection was unavailable)."
- If the proxy refuses every request, the run fails with "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".
- If no page loads and the backup connection also fails, the run fails with "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 Otto pages."
- If pages fail for another reason, the run fails with "Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search."
- A run that never requested a page (an empty search keyword, an empty URL list, or only unsupported links) is not a connection failure: it logs what to fix and ends without these messages, as does a run whose pages loaded but returned nothing.

### FAQ

#### How much does it cost?

You pay per product returned, with the detail and review enrichment billed only once per product, only when it is actually fetched and kept. The **Pricing** tab shows the current rates. Use **Max products total** to cap the cost of any run.

#### Which dataset views are available?

The dataset has these views: **Products** (overview), **Variants (nested)**, **Reviews (one row per review)**, **Product details** (needs Fetch full product detail) and **Changes (incremental mode)**.

#### What do the registered prices look like?

Pay per event, as registered on the Pricing tab (Free plan prices; higher plans pay less):

- **Product result**: $0.0018 for each product returned in the dataset.
- **Detail & review enrichment**: $0.0008 once per product when full detail and/or reviews are fetched and kept.
- **Actor Start**: $0.06 per GB of memory, one event minimum.

With the defaults (reviews on) that is roughly $2.66 per 1,000 products on the Free plan.

#### Is it legal to scrape OTTO?

This actor collects only publicly available product data. You are responsible for how you use it: follow OTTO's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Prices, availability and specifications are generally facts, but product photos 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 run then returns only new and updated products, and unchanged ones are not returned or billed unless you turn that on too.

#### Why did specialsOnly return nothing for my pasted product link?

The product detail page does not expose the same special-offer signal a listing card does, so **Specials / offers only** always excludes a single pasted product URL, whether or not that product is actually discounted. Leave it off when pasting one product link, and use it on a search or category URL or link instead.

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

If the site's response no longer matches any shape this actor recognizes, the run fails with a clear message instead of quietly returning zero products, so "no results for this search" is never confused with "the extraction broke". A genuinely empty search still completes normally with zero items. If the connection is repeatedly refused partway through, the run keeps everything already collected and stops cleanly rather than retrying forever.

#### 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 fashion data?

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

<table>
<tr><td>🔌 <a href="https://apify.com/abotapi/fnac-com-scraper"><b>Fnac Scraper</b></a><br>Scrape Fnac (fnac.com) products: current price plus strike-through discount, colour/model...</td><td>🔌 <a href="https://apify.com/abotapi/boulanger-com-scraper"><b>Boulanger.com</b></a><br>Scrape Boulanger (boulanger.com) electronics and home-appliance products: current price...</td></tr>
<tr><td>🥦 <a href="https://apify.com/abotapi/elcorteingles-es-scraper"><b>El Corte Inglés Scraper</b></a><br>Scrape El Corte Inglés elcorteingles.es products across fashion, electronics, home...</td><td>🔌 <a href="https://apify.com/abotapi/darty-com-scraper"><b>Darty Scraper</b></a><br>Scrape Darty (darty.com) products: current price plus strike-through reference price and...</td></tr>
<tr><td>🏷️ <a href="https://apify.com/abotapi/kleinanzeigen-de-scraper"><b>Kleinanzeigen.de Scraper</b></a><br>From $1/1K. Scrape Kleinanzeigen.de ads by keyword, category, location, price, seller...</td><td>🔌 <a href="https://apify.com/abotapi/currys-co-uk"><b>Currys UK Scraper</b></a><br>Scrape Currys UK electronics by keyword or URL. Extract prices, brands, stock status...</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/otto-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 or category name, with real site filters and sort. 'url' scrapes any product, search-result, or category page URL you paste, walking pagination forward automatically.

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

Free-text keyword (e.g. 'ecksofa') or a category name (e.g. 'sofas', 'laptop', 'waschmaschine') to browse a whole listing -- both go through OTTO's own search route.

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

Paste OTTO search pages (.../suche/<term>/), category pages (.../moebel/sofas/ecksofas/), department hubs (.../moebel/), with optional ?sortiertnach= sort or ?marke=\&preis= filters, or the internal /dundee/tilelist JSON route (o=offset starts mid-list = partial scope). Product pages (.../p/<slug>-<id>/) are also accepted. Leaving this field untouched scrapes one example of each shape (see README). Rival alias: startUrls.

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

Keep only products currently showing a reduced price (strike-through original price + discount %). Narrows the same search/category listing, verified live -- combine with Sort = 'Highest discount' to surface the best offers first. No extra fetch.

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

Only return products from these brands, e.g. '99rooms', 'Samsung', 'Jockenhöfer Gruppe'. Match the exact brand name as shown on the site. Leave empty for all brands.

## `minRating` (type: `integer`):

Only return products rated at or above this many stars, 1-5.

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

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

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

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

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

OTTO's own real sort order, applied server-side so paginated results stay correctly ordered across pages.

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

Adds the full breadcrumb category path, flattened technical specs, seller/partner identity (OTTO vs marketplace partner), EAN/GTIN/SKU, return policy, and full image gallery from the product detail page. Current price, was-price/discount, variant matrix and rating are already included without this toggle. A pasted product URL always fetches this page regardless of the toggle (it is the only source of that item's data) -- the toggle controls whether these extra fields are kept.

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

Fetch each product's native customer reviews: overall rating, review count, and per-review author/location/date/rating/verified-purchase/title/body/helpful-votes. Products with no reviews simply return an empty list. Shares the same product-detail fetch as 'Fetch full product detail' when both are on.

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

Cap on reviews fetched per product when 'Fetch reviews' is on. The detail page renders about a dozen; a higher cap reads further pages of OTTO's own review list. 0 = all reviews the product detail page renders.

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

Stop after this many result-page fetches per search/category/URL entry. 0 (default) = no explicit limit -- the run walks the catalogue and stops at Max products total, or at an internal hard cap of 1000 pages per entry.

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

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

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

Governs the product listing/search connection, which works on every Apify plan including the free tier. The product detail and reviews fetch (on by default) always uses a residential connection at extra cost, regardless of this setting.

## `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). Leave empty for a normal run. For recurring daily/weekly 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. A product counts as UPDATED when any returned field changes (price, original price, availability, name, images, variants, and the detail and review fields when those are on), and changedFields names them; integral prices such as 25 and 25.0 are the same value. Nothing is excluded by default; use "Ignore fields when detecting changes" to leave out a noisy field. 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 detail-mode 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 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. Top-level field names (for example promoLabel) to leave out of UPDATED change detection and out of changedFields, on top of the built-in handling. The field is still returned in the output. Use it when a field turns out to be noisy in your setup. Changing this list never causes a one-time wave of UPDATED rows.

## `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": "ecksofa",
  "urls": [
    "https://www.otto.de/suche/ecksofa/",
    "https://www.otto.de/moebel/sofas/ecksofas/",
    "https://www.otto.de/moebel/sofas/",
    "https://www.otto.de/suche/ecksofa/?sortiertnach=preis-aufsteigend",
    "https://www.otto.de/moebel/sofas/ecksofas/?sortiertnach=preis-aufsteigend",
    "https://www.otto.de/moebel/sofas/ecksofas/?marke=sit-more&preis=100-500",
    "https://www.otto.de/dundee/tilelist?rule=%28und.%28suchbegriff.ecksofa%29.%28~.%28v.1%29%29%29&sortiertnach=topseller&o=120",
    "https://www.otto.de/dundee/tilelist?rule=%28und.%28suchbegriff.ecksofa%29.%28~.%28v.1%29%29%29&sortiertnach=topseller",
    "https://otto.de/suche/ecksofa",
    "https://www.otto.de/technik/fernseher/",
    "https://www.otto.de/suche/laptop/?suchbegriff=laptop",
    "https://www.otto.de/moebel/"
  ],
  "specialsOnly": false,
  "brands": [],
  "sortBy": "RELEVANCE",
  "fetchDetails": false,
  "fetchReviews": true,
  "maxReviewsPerProduct": 10,
  "maxPages": 0,
  "maxItems": 20,
  "proxy": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 50
}
```

# Actor output Schema

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

No description

## `detail` (type: `string`):

No description

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

No description

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

No description

## `variants` (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": "ecksofa",
    "urls": [
        "https://www.otto.de/suche/ecksofa/",
        "https://www.otto.de/moebel/sofas/ecksofas/",
        "https://www.otto.de/moebel/sofas/",
        "https://www.otto.de/suche/ecksofa/?sortiertnach=preis-aufsteigend",
        "https://www.otto.de/moebel/sofas/ecksofas/?sortiertnach=preis-aufsteigend",
        "https://www.otto.de/moebel/sofas/ecksofas/?marke=sit-more&preis=100-500",
        "https://www.otto.de/dundee/tilelist?rule=%28und.%28suchbegriff.ecksofa%29.%28~.%28v.1%29%29%29&sortiertnach=topseller&o=120",
        "https://www.otto.de/dundee/tilelist?rule=%28und.%28suchbegriff.ecksofa%29.%28~.%28v.1%29%29%29&sortiertnach=topseller",
        "https://otto.de/suche/ecksofa",
        "https://www.otto.de/technik/fernseher/",
        "https://www.otto.de/suche/laptop/?suchbegriff=laptop",
        "https://www.otto.de/moebel/"
    ],
    "brands": [],
    "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/otto-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": "ecksofa",
    "urls": [
        "https://www.otto.de/suche/ecksofa/",
        "https://www.otto.de/moebel/sofas/ecksofas/",
        "https://www.otto.de/moebel/sofas/",
        "https://www.otto.de/suche/ecksofa/?sortiertnach=preis-aufsteigend",
        "https://www.otto.de/moebel/sofas/ecksofas/?sortiertnach=preis-aufsteigend",
        "https://www.otto.de/moebel/sofas/ecksofas/?marke=sit-more&preis=100-500",
        "https://www.otto.de/dundee/tilelist?rule=%28und.%28suchbegriff.ecksofa%29.%28~.%28v.1%29%29%29&sortiertnach=topseller&o=120",
        "https://www.otto.de/dundee/tilelist?rule=%28und.%28suchbegriff.ecksofa%29.%28~.%28v.1%29%29%29&sortiertnach=topseller",
        "https://otto.de/suche/ecksofa",
        "https://www.otto.de/technik/fernseher/",
        "https://www.otto.de/suche/laptop/?suchbegriff=laptop",
        "https://www.otto.de/moebel/",
    ],
    "brands": [],
    "proxy": { "useApifyProxy": True },
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "ignoreFieldsForChanges": [],
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/otto-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": "ecksofa",
  "urls": [
    "https://www.otto.de/suche/ecksofa/",
    "https://www.otto.de/moebel/sofas/ecksofas/",
    "https://www.otto.de/moebel/sofas/",
    "https://www.otto.de/suche/ecksofa/?sortiertnach=preis-aufsteigend",
    "https://www.otto.de/moebel/sofas/ecksofas/?sortiertnach=preis-aufsteigend",
    "https://www.otto.de/moebel/sofas/ecksofas/?marke=sit-more&preis=100-500",
    "https://www.otto.de/dundee/tilelist?rule=%28und.%28suchbegriff.ecksofa%29.%28~.%28v.1%29%29%29&sortiertnach=topseller&o=120",
    "https://www.otto.de/dundee/tilelist?rule=%28und.%28suchbegriff.ecksofa%29.%28~.%28v.1%29%29%29&sortiertnach=topseller",
    "https://otto.de/suche/ecksofa",
    "https://www.otto.de/technik/fernseher/",
    "https://www.otto.de/suche/laptop/?suchbegriff=laptop",
    "https://www.otto.de/moebel/"
  ],
  "brands": [],
  "proxy": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": []
}' |
apify call abotapi/otto-de-scraper --silent --output-dataset

```

## MCP server setup

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