# Otto.de Product Price Watch (Germany) (`superslowsloth/otto-de-products`) Actor

Search otto.de for any term and get one flat row per product: price in EUR, UVP and discount, rating, review count, brand, delivery note, URL, image - and whether each product is new, cheaper or dearer than on the last run.

- **URL**: https://apify.com/superslowsloth/otto-de-products.md
- **Developed by:** [Superslow Sloth](https://apify.com/superslowsloth) (community)
- **Categories:** E-commerce, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.55 / 1,000 product scrapeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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.de Product Price Watch (Germany)

Search [otto.de](https://www.otto.de) for one or more terms and get one flat
row per product: **price in EUR, UVP (the crossed-out recommended price) and
discount, rating and review count, brand, delivery note, URL and image** - and
whether each product is **new since your last run** or **changed price**.
Schedule it daily with *Only new and re-priced products* on and you get a
price-watch feed that costs nothing for products whose price has not moved.

### Input

| Field | Notes |
|---|---|
| `queries` | Search terms, one per line (`staubsauger`, `akku staubsauger`). Each is searched separately; a product two terms both find is returned once, under the first. |
| `maxItems` | Products read per search term (default 100, max 5,000). Otto serves 120 per page. |
| `sort` | `topseller` (default), `preis-aufsteigend`, `preis-absteigend`, `bewertung`, `hoechste-reduzierung`, `neuheiten` - Otto's own sort chips. The actor checks which sort the page says it served and stops with a failure on a mismatch, because Otto silently falls back to the default for a value it does not know. |
| `fullPrices` | See *Which products have a price* below. Default on. |
| `changesOnly` | Return and charge only products that are new or re-priced since the last run of the same search. |

### Which products have a price

This is the one thing to know about otto.de. Its search page is server-rendered,
but only the **first 18 of the 120 products on a page are rendered with a
price** (measured 2026-10-07 on `staubsauger`, offsets 0, 120, 240 and 3,900,
and on three other queries). The other 102 are placeholders with an id, a name
and a link; the website fills them in with a second request the moment you
scroll.

- **`fullPrices` off** reads the page exactly as it is served. Products
  with a price on the page become rows; placeholders are **skipped and not
  charged** (the run log counts them). You get the 18 priced products of every
  page - a thin slice of the result list, but each row is complete.
- **`fullPrices` on (default)** also calls `GET /crocotile/tile/data?variationIds=...` -
  the endpoint the website's own script calls to fill the placeholders in - with
  the ids of the unpriced products of each page, one extra request per page. Every
  product becomes a row. Its prices matched the page's own on all 17 products
  both sources priced.

### Output

| Field | Notes |
|---|---|
| `change` | `new`, `price_drop`, `price_rise` or `unchanged`, compared with the previous run of the same search. |
| `query`, `sort` | The term that surfaced the product first, and the sort order. |
| `position`, `sponsored` | Place in the result list (counted across pages) and whether it is a paid placement. Sponsored tiles repeat products that are also listed ordinarily; the product is returned once. |
| `product_id`, `variation_id`, `article_number` | Otto's ids. A product has one variation per colour or size; the price and the change tracking belong to the variation. |
| `name`, `brand`, `url`, `image_url` | `url` is `https://www.otto.de/p/<slug>/?variationId=<variation_id>`. |
| `price`, `currency` | The selling price as a float, EUR. |
| `is_starting_price` | True for "ab 3,48 €": the lowest of several variants' prices. |
| `suggested_retail_price` | The crossed-out UVP. Null when the tile shows none. |
| `discount_percent` | Whole percent below the UVP, computed from the two prices above. It equalled the sale tag Otto prints on every tile checked (about 110). Null without a UVP. |
| `unit_price`, `unit_price_amount`, `unit_price_unit` | "(7,00 €/ 1 Stk)" as 7.0, 1.0, "Stk". Null when Otto prints none. |
| `rating`, `review_count` | Star average and number of ratings; both null for an unreviewed product. |
| `availability`, `delivery_note` | `InStock` and Otto's delivery line, verbatim. The wording changes between requests ("in 4-5 Werktagen bei dir" / "bis Mo., 12. Okt. bei dir") - do not diff it between runs. |

#### Honest nulls

A value the tile does not show is `null`, never `0` or `""`. A product with no
UVP has `discount_percent: null`, not `0`.

#### What is not exported

Otto's own earlier price ("Vergleichspreis") exists in only one of the two
markups otto.de serves for the same tile (chosen per request: six identical
requests gave one markup four times and the other twice), so it would be filled
on half the runs and read as a price change on the others. It is left out.

#### A search never comes back empty

A nonsense term is not a "no results" page on otto.de: `xqzvbnmkjhg` returned
1,333 unrelated products (a loose match; the nonsense word is still printed in
the heading). Check your terms; the actor cannot tell that from a real hit.

### Change tracking

Each search (the set of terms and the sort order) has its own memory, kept in a
named key-value store on **your** Apify account (`otto-de-products-watch`), so
it is never visible to anyone else. The first run labels every product `new`.
After that a product is `new` if the last run did not read it, `price_drop` /
`price_rise` if its price moved, and `unchanged` otherwise. The memory keys on
the **variation id**, not the product id, so a tile that starts showing a
different colour is `new` rather than a fake price change.

With `changesOnly`, `unchanged` products are neither returned nor charged.
`maxItems` still counts them: it is how much of the result list is watched, so
a watch of the top 500 results reads 500 however few moved. Otto's lists are
not newest-first (the default is *Topseller*), so there is no early stop - the
run reads `maxItems` products per term.

### Pricing

Pay per event:

- **$0.002** per run (`actor-start`), charged only after your input is
  validated - a run that fails on bad input costs nothing.
- **$0.00055** per product row written (`product-scraped`), i.e. $0.55 per
  1,000 products. With `changesOnly`, unchanged products are not charged.

### How it fetches

It reads the same pages otto.de serves to a browser
(`https://www.otto.de/suche/<term>/?l=gq&o=<offset>&sortiertnach=<sort>`, 120
products per page), parsing the product JSON-LD and price markup of each tile;
with `fullPrices` it adds one request per page to `/crocotile/tile/data`. No
account, no login, no cookies. Apify datacenter proxy is the default; a refused
request (HTTP 403, 429, 5xx, or a 200 page without results) is retried from a
fresh address.

# Actor input Schema

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

What you would type into otto.de's search box, one term per line, in German or any language otto.de understands (for example staubsauger or akku staubsauger). Each term is searched and read separately. A product that two terms both find is returned once, under the first.

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

Read at most this many products for each search term. Otto serves 120 products per page. With 'Only new and re-priced products' on, unchanged products still count toward this limit - it is how much of the result list is watched - but are not returned or charged.

## `sort` (type: `string`):

The order otto.de lists the results in. Topseller is the default. 'Niedrigster Preis' and 'Höchster Preis' sort by price, 'Höchste Reduzierung' by discount. Change tracking is per sort order, so changing it starts a fresh memory.

## `fullPrices` (type: `boolean`):

otto.de's search page only prices the first 18 of its 120 products per page; the other 102 are placeholders that the website fills in with a second request. Off: only the products the page itself prices are returned, and unpriced placeholders are skipped and not charged. On (the default): the prices of the placeholders are fetched from the endpoint the website itself calls (/crocotile/tile/data), so every product becomes a row - one extra request per page.

## `changesOnly` (type: `boolean`):

Return (and charge for) only products that are new, or whose price changed, since the last run of this same search; unchanged ones are skipped and cost nothing. The memory is per set of search terms and sort order. The FIRST run has nothing to compare against, so it labels every product new and returns them all.

## `proxyConfiguration` (type: `object`):

Apify proxy (automatic datacenter exits) is the default. A proxy is what lets a refused request be retried from a fresh address.

## Actor input object example

```json
{
  "queries": [
    "staubsauger"
  ],
  "maxItems": 100,
  "sort": "topseller",
  "fullPrices": true,
  "changesOnly": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `items` (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 = {
    "queries": [
        "staubsauger"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("superslowsloth/otto-de-products").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 = {
    "queries": ["staubsauger"],
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("superslowsloth/otto-de-products").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 '{
  "queries": [
    "staubsauger"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call superslowsloth/otto-de-products --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,superslowsloth/otto-de-products"
        }
    }
}
```

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/EZfvkBOKlhmCXqW8o/builds/0P1ugJxzRgRAWXt2s/openapi.json
