# REWE.de Scraper - Prices, Offers & Postal-Code Matching (`abotapi/rewe-de-scraper`) Actor

Scrape REWE Germany (rewe.de) grocery products: current price, was-price and discount when genuinely on offer, brand, category, price per unit, images and tags. Prices match a real postal code you choose. Search keywords or paste product/listing links.

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

## Pricing

from $1.00 / 1,000 product results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

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

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

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

# README

## REWE.de Grocery Scraper

Pull grocery product data from REWE Germany (rewe.de). Search by keyword and/or REWE's own category, brand, and dietary/sourcing filters, or paste product and listing links directly. Prices and offers are matched to a real German postal code you choose, the same way the site itself scopes pricing to your local market.

### Why This Scraper?

- **Postal-code matched pricing.** Give it a German postal code (PLZ) and it resolves your local market before pulling prices, the same step the site itself requires before showing you real numbers.
- **Was-price and discount, gated on the site's own signal, never guessed.** When a product genuinely carries a reduced price, the original price plus the discount amount and percentage are captured. A full-price product simply carries `originalPrice: null` -- nothing is fabricated.
- **Real site filters.** Category, brand, and REWE's own dietary/sourcing quick filters (Bio, Vegan, Regional, New, Gekühlt, Tiefgefroren) -- all verified live against the site's own controls, not guessed.
- **Two ways in.** Keyword and/or category search, or paste any product or listing link.
- **Optional export to your apps.** Send results into Notion, Linear, Airtable, or any Apify MCP connector alongside the dataset.
- **Resume & recurring updates** (optional) -- turn on Incremental mode to get only NEW, UPDATED and REAPPEARED products on every scheduled run, or resume one specific interrupted crawl with `resumeFromRunId`.

### Data You Get

| Field | Example value |
|---|---|
| productId | `1042422` |
| name | `Sample Fresh Whole Milk 3.7% 1l` |
| brand | `Sample Dairy` |
| category / categoryPath | `Frischmilch`, `["Cheese, Eggs & Dairy", "Milk", "Frischmilch"]` |
| tags | `["Gekühlt", "Regional"]` |
| url | `https://www.rewe.de/p/sample-fresh-whole-milk-1042422/1042422` |
| price / currency | `1.29`, `EUR` |
| originalPrice / discountAmount / discountPercent | `1.59`, `0.30`, `19` |
| isOnSpecial | `true` |
| pricePerUnit / grammage | `1,29 € / 1l`, `1l` |
| available | `true` |
| image / images | `["https://img.rewe-static.de/sample/digital-image.png"]` |
| description / ingredients | *(when Fetch product detail is on)* |

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

### How to Use

**1. Keyword search, first page (default):**

```json
{
  "mode": "search",
  "searchTerm": "milch",
  "zipCode": "50667",
  "maxItems": 20
}
```

**2. Category browse, filtered to a brand and dietary tag:**

```json
{
  "mode": "search",
  "categorySlug": "milch",
  "brand": "Alpro",
  "attribute": "VEGAN",
  "zipCode": "50667",
  "maxItems": 50,
  "maxPages": 5
}
```

**3. Paste product or listing links and set your postal code:**

```json
{
  "mode": "url",
  "urls": ["https://www.rewe.de/p/sample-fresh-whole-milk-1042422/1042422"],
  "zipCode": "50667",
  "fetchDetails": true
}
```

### Input Parameters

| Parameter | Type | Description |
|---|---|---|
| `mode` | string | `search` or `url`. |
| `searchTerm` | string | Free-text keyword (search mode only). |
| `categorySlug` | string | REWE's own category token, e.g. `milch` (search mode only). |
| `brand` | string | Exact brand name to keep (applies in both search mode and URL mode). |
| `attribute` | string | `ORGANIC`, `VEGAN`, `REGIONAL`, `NEW`, `CHILLED`, or `FROZEN` -- REWE's own dietary/sourcing quick filter (applies in both search mode and URL mode). |
| `minPrice` / `maxPrice` | number | EUR price band to keep (applies in both search mode and URL mode). REWE offers no price filter of its own, so a very tight band can end a search early -- see "How filtering is bounded" below. |
| `urls` | array | Product or listing URLs to scrape (url mode only). |
| `fetchDetails` | boolean | Fetch description and ingredient statement from the product detail page. |
| `maxPages` | integer | Cap on result pages per search/URL entry; empty = no page cap (bounded by `Max products` and the guards below, never truncated silently). |
| `maxItems` | integer | Cap on total products returned; `0` = unlimited. |
| `zipCode` | string | German postal code REWE uses to match your local market and its prices. |
| `resumeFromRunId` | string | Continue one interrupted crawl -- ID of a previous run/dataset; already-collected products are skipped. See Resume & recurring updates below. |
| `incrementalMode` | boolean | Recurring/scheduled monitoring of the same search -- only NEW/UPDATED/REAPPEARED products are returned by default. Default `false`. |
| `stateKey` | string | Optional name for an incremental-mode monitoring campaign. Leave empty to derive one automatically from mode/postal code/search/filter settings. |
| `emitUnchanged` | boolean | Incremental mode only. Also return (and bill) products unchanged since the last run. Default `false`. |
| `emitExpired` | boolean | Incremental mode only. Also return (and bill) products no longer found, once a run fully scans the tracked search. Default `false`. |
| `proxy` | object | Apify proxy configuration. A residential connection is required for a reliable run (see below). |
| `mcpConnectors` | array | Optional MCP connectors to export results into (Notion, Linear, Airtable, Apify). |
| `notionParentPageUrl` | string | Notion connector only: page under which item pages are created. |
| `maxNotifyListings` | integer | Cap on items exported to each connector per run. Does not affect the dataset. |

### Resume & recurring updates

Two different things -- pick the one that matches what you're doing:

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

**Resume** (`resumeFromRunId`) continues one specific interrupted crawl: paste a run ID or dataset ID and this run skips products already collected there, returning only the remaining new products.

**Incremental mode** (`incrementalMode`) is for a schedule (e.g. daily): the actor remembers the previous run of the *same* search by itself, so you never paste a run ID. The first run returns everything as `NEW`. Later runs return only `NEW`, `UPDATED` and `REAPPEARED` products by default -- unchanged products are suppressed (and not charged). Turn on `emitUnchanged` or `emitExpired` only when you also want those rows returned (and billed for). State is isolated automatically per mode/postal code/search term/category/brand/attribute/price filters/URLs/detail setting -- `zipCode` is included because it selects a different REWE market (different prices, different assortment); set `stateKey` to name or deliberately share a monitoring campaign. `EXPIRED` is only synthesized once a run fully scans the tracked search (not capped by Max products/Max pages, not stopped early by a connection problem, not a Resume run).

When `incrementalMode` is on, every returned record also carries:

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

If **Fetch product detail** is on and a detail-page fetch fails on a later run, the previously-fetched `description`/`ingredients` are carried forward from state rather than dropped -- a transient fetch failure never looks like real content disappearing (and never re-bills the detail-enrichment surcharge for it).

### Output Example

```json
{
  "productId": "1042422",
  "name": "Sample Fresh Whole Milk 3.7% 1l",
  "brand": "Sample Dairy",
  "tags": ["Gekühlt", "Regional"],
  "url": "https://www.rewe.de/p/sample-fresh-whole-milk-1042422/1042422",
  "hasVariants": false,
  "freeShipping": false,
  "categoryId": "175",
  "category": "Frischmilch",
  "categoryPath": ["Cheese, Eggs & Dairy", "Milk", "Frischmilch"],
  "image": "https://img.rewe-static.de/sample/digital-image.png",
  "images": ["https://img.rewe-static.de/sample/digital-image.png"],
  "price": 1.29,
  "currency": "EUR",
  "originalPrice": 1.59,
  "discountAmount": 0.3,
  "discountPercent": 19,
  "isOnSpecial": true,
  "pricePerUnit": "1,29 € / 1l",
  "grammage": "1l",
  "available": true
}
```

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

### How filtering is bounded

REWE offers no price filter of its own, so a price band narrows what this actor returns to you
rather than what REWE hands back. To stop a very selective band from running through an entire
category or search looking for survivors, a search stops after **5 consecutive result pages that
contained new (not-yet-seen) products but none of them matched the price band**.

Result pages with nothing new on them at all -- for example, when you resume a previous run with
`resumeFromRunId` and its early results are already collected -- never count toward this limit;
only your price band can trip it, and only once it has actually had unseen products to discard.

The run log tells you which of two things happened:

- *"Page N: ... product(s)"* followed by a further page -- your results were still coming in.
- *"Stopping this search after page N: the last 5 page(s) contained new products but none matched
  your price filter."* -- the run gave up on that 5-page rule. These results were **not** read to
  the end; more matches may exist further down. Narrow the search/category or widen the price band
  to see more.

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

Optionally pipe results into the apps you already use through Model Context Protocol (MCP) connectors. Authorize a connector once under Apify, Settings, Integrations, then select it in the `mcpConnectors` field. Each connector receives a condensed, human-readable summary per product (name plus key fields), while the complete record always stays in the Apify dataset. For Notion, set `notionParentPageUrl` to the page the item pages should be created under, and use `maxNotifyListings` to cap how many items are exported per connector per run. Supported connectors: Notion, Linear, Airtable, and Apify. Leave the field empty to skip; it never changes the dataset output.

### A note on plans

rewe.de requires a **residential** connection for a reliable run -- a datacenter or free-tier connection will very likely fail. The default proxy configuration already selects a residential connection in Germany; no extra setup is needed on any paid plan with residential proxy access.

# Actor input Schema

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

'search' finds products by keyword and/or REWE's own category, brand, and attribute filters. 'url' scrapes any product or listing page URL you paste.

## `zipCode` (type: `string`):

German postal code REWE uses to match your local market. Prices, offers and availability are scoped to it, in every mode - and it also decides which products the market stocks, so it changes the result set, not just the prices. Five digits, e.g. 50667 for Cologne. A code with no REWE market stops the run rather than returning products with no prices.

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

Free-text keyword, e.g. 'milch' or 'kaffee'. Leave empty to browse a whole category with the field below.

## `categorySlug` (type: `string`):

REWE's own category token, e.g. 'milch' narrows to the Milch category. Combine with a keyword to search within a category, or leave the keyword empty to browse the whole category. Search mode only: URL mode reads this from the link you paste.

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

Paste one or more REWE product pages (.../p/<slug>/<id>) or listing pages (a URL with a `search=` or `categorySlug=` query). Search-mode fields are ignored here. The fields in the Filters section still apply, including to a pasted product URL.

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

Only return products from this brand, matched exactly as REWE's own brand filter shows it, e.g. 'Lindt'. Leave empty for all brands. Applies in BOTH search mode and URL mode: the listing request is rebuilt for a pasted category or search URL, so the filter is sent with it.

## `attribute` (type: `string`):

REWE's own quick filter -- narrows to products carrying that tag. Applies in BOTH search mode and URL mode: the listing request is rebuilt for a pasted category or search URL, so the filter is sent with it.

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

Only return products priced at or above this amount, in euros. Applies in BOTH search mode and URL mode: the listing request is rebuilt for a pasted category or search URL, so the filter is sent with it.

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

Only return products priced at or below this amount, in euros. Applies in BOTH search mode and URL mode: the listing request is rebuilt for a pasted category or search URL, so the filter is sent with it.

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

Adds the product description and ingredient statement from the product detail page, when REWE's own page carries them. Current price, was-price/discount, brand, category and images are already included without this toggle.

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

Stop after this many result pages per search/URL entry. Leave empty for no page limit -- the run stops at Max products total.

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

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

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

Optional. ID of a previous run of this actor (or a dataset ID). Products already in that dataset are skipped, so this run returns only NEW products (a delta). Combine both runs' datasets for the full set. For recurring monitoring of the same search, use Incremental mode below instead.

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

Turn this on for daily or recurring monitoring of the same search/URLs. The first run returns all matching products as NEW. Later runs normally return only NEW, UPDATED and REAPPEARED products. Turn on "Emit unchanged" or "Emit expired" only when you also want those products returned (and billed). State is kept separately per distinct search/filter/postal-code setup (or by State key below).

## `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 your mode/postal code/search/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/Max pages capped it, a connection problem stopped it early, or Resume was used. This returns -- and bills -- extra synthetic rows.

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

A residential connection is required for a reliable run on this site. The default below already selects a residential connection in Germany.

## `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",
  "zipCode": "50667",
  "searchTerm": "milch",
  "urls": [
    "https://www.rewe.de/p/weihenstephan-h-milch-3-5-1l/677067"
  ],
  "attribute": "",
  "fetchDetails": false,
  "maxItems": 20,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "DE"
  },
  "maxNotifyListings": 50
}
```

# Actor output Schema

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

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "mode": "search",
    "zipCode": "50667",
    "searchTerm": "milch",
    "urls": [
        "https://www.rewe.de/p/weihenstephan-h-milch-3-5-1l/677067"
    ],
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "DE"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/rewe-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",
    "zipCode": "50667",
    "searchTerm": "milch",
    "urls": ["https://www.rewe.de/p/weihenstephan-h-milch-3-5-1l/677067"],
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "DE",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/rewe-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",
  "zipCode": "50667",
  "searchTerm": "milch",
  "urls": [
    "https://www.rewe.de/p/weihenstephan-h-milch-3-5-1l/677067"
  ],
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "DE"
  }
}' |
apify call abotapi/rewe-de-scraper --silent --output-dataset

```

## MCP server setup

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