# MediaMarkt.es Scraper - Prices, Offers & Reviews (`abotapi/mediamarkt-es-scraper`) Actor

Scrape MediaMarkt Spain (mediamarkt.es) products: current price, strike-through original price + discount on offers, brand, EAN, category path, availability, image gallery, full spec sheet, other-seller offers, and reviews with ratings. Search keywords or paste links.

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

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

## MediaMarkt.es Product Scraper: Prices, Offers & Reviews

MediaMarkt.es Product Scraper turns MediaMarkt Spain (mediamarkt.es) into structured product data. Get current price, the strike-through original price and discount when a product is genuinely on offer, brand, EAN, category path, availability, the full image gallery, a category-specific spec sheet, marketplace seller offers, and customer reviews with ratings. Search by keyword and category, or paste product, category, and search-listing links, then export to JSON, CSV or Excel, or pull the results straight into your app through the API.

### Why This Scraper?

- **Was-price and discount, gated on the site's own signal, never guessed.** When a product carries a struck-through original price, both prices are captured plus the discount amount and percentage. A full-price product simply carries `originalPrice: null`, nothing fabricated.
- **Real customer reviews.** Average rating, total review count, and each product's most recent reviews with rating, title, full text, date, and a verified-purchaser flag.
- **Full spec sheet on demand.** The flattened characteristics MediaMarkt itself shows per product (RAM/CPU/display for a laptop, panel type/resolution for a TV, and so on), never hardcoded to one category's fields.
- **Marketplace-aware.** Distinguishes MediaMarkt's own retail offers from third-party marketplace sellers, including seller name and rating when a listing is a marketplace offer.
- **Real site filters and sort.** Brand, price range, and MediaMarkt's own sort order (price, rating, topseller, newest, availability), matching the site's own controls, plus a minimum-rating filter.
- **Two ways in.** Keyword and/or category search, or paste any product, category, or listing link and pagination continues forward automatically.
- **Optional export to your apps.** Send results into Notion, Linear, Airtable, or any Apify MCP connector alongside the dataset.

### Use Cases

- **Price tracking and comparison:** collect current price, strike-through original price and discount percentage across a catalog to power price-history charts and deal alerts.
- **Marketplace and reseller research:** compare MediaMarkt's own retail offers against third-party marketplace sellers, including seller name and rating, before sourcing or repricing.
- **Product research and spec comparison:** pull the full flattened spec sheet for laptops, TVs, and appliances to build comparison tables and buying guides.
- **Review and reputation analysis:** collect ratings, review counts, and individual reviews to track sentiment and flag quality issues per product or brand.
- **Assortment and availability monitoring:** track online availability, delivery, and pickup status across a category or brand to spot stockouts and restocks.
- **Deal hunting:** filter by brand, price range, and rating, sorted by the site's own price or topseller order, to surface the current best offers.

### Data You Get

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

| Field | Example |
| --- | --- |
| `productId` / `sku` | `"9000001"` |
| `ean` | `"8400000000158"` |
| `name` | `"Sample Laptop 15 pulgadas"` |
| `brand` | `"Sample Brand"` |
| `category` / `categoryPath` | `"Portatiles"` / `["Informatica", "Portatiles"]` |
| `url` | `"https://www.mediamarkt.es/es/product/_sample-laptop-9000001.html"` |
| `price` / `currency` | `549` / `"EUR"` |
| `originalPrice` / `discountAmount` / `discountPercent` | `649` / `100` / `15` (all `null` off a genuine offer) |
| `isOnSpecial` / `strikePriceType` | `true` / `"XOP"` |
| `onlineAvailable` / `availabilityStatus` | `true` / `"ONLINE"` |
| `isMarketplace` | `false` |
| `marketplaceSeller` | `{"sellerId": "S1", "sellerName": "Sample Seller", "sellerRating": 4.2}` (marketplace offers only) |
| `image` / `images` | `"https://assets.mmsrg.com/isr/sample/ASSET_ONE"` / full gallery array |
| `specs` | `{"Tamano de pantalla": "15.6 pulgadas", "Memoria RAM": "16 GB"}` (flattened, varies per category, when Fetch full product detail is on) |
| `delivery` | `{"status": "AVAILABLE", "displayStatus": "En stock", "earliestDate": "2026-10-02", "latestDate": "2026-10-04"}` (when Fetch full product detail is on) |
| `pickup` | `{"status": "AVAILABLE", "displayStatus": "Recogida hoy", "isPickable": true}` (when Fetch full product detail is on) |
| `otherOffers` | other sellers' offers for the same product, when present and Fetch full product detail is on |
| `rating` / `reviewCount` | `4.2` / `30` |
| `reviews` | `{"averageRating": 4.2, "totalReviewCount": 30, "items": [{"rating": 5, "title": "Sample title", "text": "Sample review text.", "reviewerName": "Sample User", "date": "2026-08-01T10:00:00.000Z", "isVerifiedPurchaser": true}]}` (items list is empty unless Fetch reviews is on) |

### How to Use

1. Pick a **mode**: `search` (keyword and/or category id, with the site's own filters and sort) or `url` (paste product, category, or search-listing links).
2. Fill in the fields for that mode, and switch on **Fetch full product detail** and/or **Fetch reviews** for the extra data you need.
3. Set **Max products total** (and optionally **Max pages per search**) 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 (default):**

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

**Category browse, filtered to a brand and price range, sorted by price:**

```json
{
  "mode": "search",
  "categoryId": "153",
  "brands": ["ASUS"],
  "minPrice": 300,
  "maxPrice": 900,
  "sortBy": "PRICE_ASC",
  "maxItems": 50,
  "maxPages": 5
}
```

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

```json
{
  "mode": "url",
  "urls": ["https://www.mediamarkt.es/es/product/_sample-laptop-9000001.html"],
  "fetchDetails": true,
  "fetchReviews": true,
  "maxReviewsPerProduct": 20
}
```

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

```json
{
  "mode": "url",
  "urls": ["https://www.mediamarkt.es/es/search.html?query=cafetera"],
  "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/mediamarkt-es-scraper").call(run_input={"mode": "search", "searchTerm": "portatil", "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/mediamarkt-es-scraper').call({ mode: 'search', searchTerm: 'portatil', 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.

#### How Max products total splits across pasted links

URL mode with the Links field left untouched scrapes one example of each supported link shape: a category page, the same category's page 2, a category by bare id (`c-<id>`), a product page (full slug), the same product by bare id (`_p-<id>`), a search listing and that search's page 2. The product pair resolves to one product and the `c-153` / `portatiles-153` pair to one listing, so duplicates are skipped. Replace them with your own links.

In URL mode, when you paste more than one product, category, or search link, Max products total is divided evenly across those links before the run starts, and that split is not adjusted afterward. If one link only has a handful of matching products, its unused share is not handed to another link with plenty of supply, so the run can finish below Max products total even though enough products existed overall. For a precise total, paste one link per run, or set Max products total generously so no single link's fixed share becomes the bottleneck.

Pasted links that overlap can also come up short in the same run. When a category or search link reaches its share partway through a results page, the products on that page it did not take are still treated as already handled. A later pasted link that contains those same products, such as a second listing with overlapping results or a product link that appeared on that page, then skips them without collecting or billing them. Example: a category link followed by a link to a product on the category's first page, with Max products total set to 2, returns only 1 product. To avoid this, paste non-overlapping links, put individual product links before listing links, or run overlapping links in separate runs.

#### Incremental mode (scheduled runs)

Turn on **Incremental mode** and the actor remembers the products it saw, so a scheduled run returns only what changed. Every row then carries `changeType` (`NEW`, `UPDATED`, `REAPPEARED` or `EXPIRED`, plus `UNCHANGED` if you ask for it), `changedFields` (the names of the fields that differ), `firstSeenAt` and `lastSeenAt`. All row fields are compared except the scrape timestamp, the review list (a new review still shows up through `rating` and `reviewCount`), the key-feature list `highlights` (it is derived from the spec sheet) and the day-relative delivery fields `deliveryStatus`, `deliveryDisplayStatus`, `delivery`, `fulfillmentEarliest` and `fulfillmentLatest`, which move every day without the product changing; `isAvailableForDelivery` is still compared, so a real availability flip is reported. Use **Ignore fields when detecting changes** for fields you do not want to count, for example `price`.

- Unchanged products are not emitted and not billed (a row is only billed when it is delivered). The product page may still be read to compare it.
- `EXPIRED` rows (products that disappeared from the results) are only reported after a run that walked the whole result list: set **Max products total** and **Max pages per search** to `0`. A run cut short by a cap, a resume id or an unavailable page never marks anything EXPIRED. Each EXPIRED row is a normal delivered result row and counts toward Max products total.
- The remembered state belongs to your search, filters and detail/review options. A different search gets its own baseline; set **State key** to keep one baseline for a task you edit. Changing caps or proxy never starts a new baseline.
- A search-mode run with no keyword, Search keywords or Category id searches the example keyword and never reads or saves state, so it cannot change or expire a baseline.
- With incremental mode off, output is exactly as before. Combining it with a resume id works but skips the EXPIRED report.

#### Resume and automatic recovery

`resumeFromRunId` continues a previous run: point it at that run's or dataset's id and products already saved there are skipped, so this run returns, and bills, only products missing from it. This works the same way whether the earlier products came from a search, a category, or a pasted product link.

A run that is interrupted by an Apify platform migration, or restarted with the Resurrect button after it failed or was aborted, also recovers on its own from an internal checkpoint, in the same run and dataset, with no duplicate products and no double billing. That checkpoint records every product the run had started on, not only the ones it saved. The continued run therefore never retries a product that was still being fetched at the moment of the interruption (with Fetch reviews on, the default, that can be a whole results page of up to 12 products), a pasted product page that failed to load, or a product left behind by the overlapping-links case above. Those products are never collected or billed in that run, and the run can finish with fewer products than Max products total even though more were available. This only affects the continuation of that same run: a brand-new run starts from scratch. If a run finishes short after a migration or Resurrect, start a new run with `resumeFromRunId` set to the earlier run's id. It checks the products actually saved in the dataset and collects, and bills, only the missing ones. Rerunning the identical input without `resumeFromRunId` also recovers them, but collects and bills everything again.

### 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 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 haven't 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 and/or category id, with the site's own filters and sort) or `url` (paste product, category, or search-listing links; pagination continues forward automatically). |
| `searchTerm` | string | `portatil` (prefill) | Free-text keyword. Leave empty if you set a Category id below. If search mode gets no keyword, no Search keywords and no Category id, the run searches the example keyword `portatil` instead of returning nothing; it is billed like any run (rows delivered), and with Incremental mode on it neither reads nor saves state. Search mode only. |
| `categoryId` | string | (none) | Numeric id (`153`), a pasted category link, or a plain category name such as `portatiles` (a plain name is used as the keyword when the keyword is empty). 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 pasted link. |
| `urls` | array | one example of each supported link shape (category page, category page 2, category by bare id, product page, product by bare id, search listing, search page 2) | Product, category, or search-listing links to scrape. Pagination continues forward automatically from any `page=` already in the URL. Search-mode fields are ignored here; the Filters and sort fields below still apply, including to a pasted product link. An empty list in URL mode stops the run with an input message. |
| `brands` | array | (none) | Only return products from these exact brand names, e.g. `APPLE`, `LENOVO`, `ASUS`, `HP`. Leave empty for all brands. Applies in both search mode and URL mode. |
| `minRating` | integer | (none) | Only return products rated at or above this many stars, 1-5. Applies in both search mode and URL mode. |
| `condition` | string | `all` | `new` or `refurbished`, decided from the product's own offer (its default offer is the refurbished one, as marketplace refurbished units are), falling back to the site's "reacondicionado" wording in the name or category path. Applied before rows are counted or billed. |
| `sellerType` | string | `all` | `mediamarkt` or `marketplace` (third-party sellers). Applied before rows are counted or billed. |
| `searchQueries` | array | (none) | Several keywords in one run; results are de-duplicated and `maxItems` caps the whole run. |
| `sortOrder` | string | (none) | Alias of `sortBy` that also accepts lowercase tokens such as `price_asc`. |
| Extra output fields | | | `seller`, `isAvailableForDelivery`, `refurbishedAvailable` (a refurbished offer exists for the product), `energyClass`, `fulfillmentEarliest`, `fulfillmentLatest`, `condition`, `deliveryStatus`, `pickupStatus` are filled only where the page carries them, otherwise null. `highlights` is the site's own short key-feature list for the product (feature name to value), null when the page shows none. |
| `minPrice` | number | (none) | Only return products priced at or above this amount, in euros. Applies in both search mode and URL mode. |
| `maxPrice` | number | (none) | Only return products priced at or below this amount, in euros. Applies in both search mode and URL mode. |
| `sortBy` | string | `RELEVANCE` | MediaMarkt's own sort order: `RELEVANCE`, `PRICE_ASC`, `PRICE_DESC`, `RATING`, `TOPSELLER`, `NEWEST`, or `AVAILABILITY`. Reorders the full server-side result set, so it stays correct across pagination. Applies in both search mode and URL mode. |
| `fetchDetails` | boolean | `false` | Adds the full flattened spec sheet, delivery/pickup availability detail, energy efficiency label, promo badges, and marketplace/other-seller offers from the product page. Current price, was-price/discount, rating, and main image are already included without this toggle. |
| `fetchReviews` | boolean | `true` | Fetch each product's review summary (average rating, total count) and its most recent reviews. Returns the site's first review page, capped by Max reviews per product; products with no reviews return an empty list. |
| `maxReviewsPerProduct` | integer | `20` | Cap on reviews returned per product when Fetch reviews is on (values above 50 are treated as 50). `0` = all available on that page. The site's product page currently shows about 10 reviews, so that is the practical ceiling. |
| `maxPages` | integer | `0` | Stop after this many result pages per search/category/URL entry. `0` = unlimited, walk every page until the site stops returning new products or Max products total is hit. |
| `maxItems` | integer | `20` | Hard cap on total products returned across every search/category/URL entry. `0` = unlimited (still bounded by Max pages per search). |
| `incrementalMode` | boolean | `false` | Remember products between runs and emit only changes (see Incremental mode above). |
| `stateKey` | string | (auto) | Name for the remembered state; empty derives it from your search and filters. With Incremental mode on and your own State key, search mode needs a keyword, Search keywords or Category id: with none, the run stops with an input message and collects nothing. |
| `emitUnchanged` | boolean | `false` | Also emit products that did not change (`changeType` UNCHANGED). |
| `emitExpired` | boolean | `true` | After a complete run, emit EXPIRED rows for products no longer listed. |
| `ignoreFieldsForChanges` | array | (empty) | Field names that must not count as a change. |
| `resumeFromRunId` | string | (none) | A previous run id (or its dataset id) to continue: products already collected there are skipped, so this run only returns new products. Leave empty for a normal run. |
| `proxy` | object | residential, Spain | Connection settings. A residential connection is required for a reliable run; the default already selects one in Spain. |
| `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 written to each connector per run. Does not affect the dataset. |

### Output Example

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

```json
{
  "productId": "9000001",
  "sku": "9000001",
  "ean": "8400000000158",
  "name": "Sample Laptop 15 pulgadas",
  "brand": "Sample Brand",
  "category": "Portatiles",
  "categoryPath": ["Informatica", "Portatiles"],
  "url": "https://www.mediamarkt.es/es/product/_sample-laptop-9000001.html",
  "price": 549,
  "currency": "EUR",
  "originalPrice": 649,
  "discountAmount": 100,
  "discountPercent": 15,
  "isOnSpecial": true,
  "strikePriceType": "XOP",
  "onlineAvailable": true,
  "availabilityStatus": "ONLINE",
  "isMarketplace": false,
  "image": "https://assets.mmsrg.com/isr/sample/ASSET_ONE",
  "images": ["https://assets.mmsrg.com/isr/sample/ASSET_ONE"],
  "rating": 4.2,
  "reviewCount": 30,
  "reviews": {
    "averageRating": 4.2,
    "totalReviewCount": 30,
    "items": [
      {
        "rating": 5,
        "title": "Sample title",
        "text": "Sample review text.",
        "reviewerName": "Sample User",
        "date": "2026-08-01T10:00:00.000Z",
        "isVerifiedPurchaser": true
      }
    ]
  }
}
```

### Plan Requirement

MediaMarkt.es answers plain and datacenter connections with an interactive challenge page, so a residential connection in Spain is required for a reliable run. The default proxy configuration already selects it, so make sure your plan includes residential proxy access.

If residential access is refused for your account (for example on a free plan), the run logs a one-time notice that the selected connection is unavailable, continues on a backup connection, and logs "Served over the backup connection (the default connection was unavailable)." once a page loads that way. Some pages may still be unavailable on the backup connection. When your residential access works but the site refuses a page on two residential exits in a row, that page is tried on up to three fresh exits of a backup residential connection in Spain before the run moves on, and the same "Served over the backup connection" line is logged once. If you enter your own proxy URLs, no backup connection is used.

### FAQ

#### How much does it cost?

You pay per product returned, plus one extra charge per product when its full spec sheet, delivery/pickup detail, and/or reviews were actually fetched (never twice for the same product). That extra charge is one unit per started block of 10 delivered reviews, with a minimum of one unit for the product-page read: 0 to 10 reviews bill 1 unit, 11 to 20 bill 2, and 25 bill 3. Reviews count only as many as are delivered (Max reviews per product defaults to 20, and the site's page currently shows about 10), so most products bill one unit. The charge is recorded for each product as soon as it is saved to the dataset; a product whose page did not load is not charged for it. The **Pricing** tab shows the current rates. Use **Max products total** to cap the cost of any run.

#### Is it legal to scrape mediamarkt.es?

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

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

Yes. Turn on **Incremental mode** (see above) and schedule the actor: each run returns only new, updated, reappeared and (after a full walk) expired products, and unchanged products are not billed. `resumeFromRunId` is a different tool, for continuing one specific earlier run.

#### Do the brand, price, and rating filters work when I paste a link instead of searching?

Yes. Pasting a category or search-listing link rebuilds the same filtered, sorted request the site's own controls would send, and a pasted product link is checked against the same filters before it is returned.

#### Why did a run finish with fewer products than Max products total when I pasted several links?

Max products total is split evenly across the pasted links up front and is not redistributed afterward, so a link with little supply cannot free up its unused share for another link with plenty. Separately, if the links overlap, products a listing link saw but did not take once its share was used up are skipped by later links too. Paste one non-overlapping link per run, or set Max products total generously, if you need a precise count.

#### Why might some products be missing after a run is interrupted and automatically resumes?

An automatic recovery after a platform migration or a Resurrect keeps track of every product the run had started on, not only the ones it actually saved. A product still being fetched when the run was interrupted, or a pasted product page that failed to load, is not retried in that continued run and is never billed. If you suspect products are missing, start a new run with `resumeFromRunId` pointed at the earlier run: it checks the dataset itself and collects only what is genuinely missing. Rerunning the identical input without it also recovers them, but collects and bills everything again.

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

If the `resumeFromRunId` you supplied cannot be resolved to a run or dataset from your account, the run fails with a clear message before collecting anything, instead of silently collecting and billing everything again. A run where no page could be loaded at all ends as failed with a plain status message instead of an empty success: one message when the proxy connection is refused for the account (a paid plan is required), one when residential access is unavailable and the backup connection could not load the pages, and one when the requests failed for another reason (a connection problem, not an empty search; try again later). If the site refuses a sustained share of requests mid-run, the run stops collecting further pages but keeps and returns everything already gathered.

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

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

<table>
<tr><td>🛒 <a href="https://apify.com/abotapi/mediamarkt-de-scraper"><b>MediaMarkt Germany Scraper</b></a><br>Scrape MediaMarkt.de products by keyword or URL. Extract current and original prices...</td><td>🔌 <a href="https://apify.com/abotapi/leroymerlin-es-scraper"><b>LeroyMerlin.es Scraper</b></a><br>Scrape Leroy Merlin Spain (leroymerlin.es) DIY and home-improvement products: price...</td></tr>
<tr><td>🛒 <a href="https://apify.com/abotapi/decathlon-es-scraper"><b>Decathlon.es</b></a><br>Scrape Decathlon Spain (decathlon.es) sporting goods: current price plus strike-through...</td><td>🥦 <a href="https://apify.com/abotapi/mercadona-es-scraper"><b>Mercadona Scraper</b></a><br>Scrape Mercadona products by delivery-area postal code. Search by keyword or category, or...</td></tr>
<tr><td>🛒 <a href="https://apify.com/abotapi/druni-es-scraper"><b>druni.es</b></a><br>Scrape Druni beauty, cosmetics and perfume products. Search by keyword, category, Ofertas...</td><td>🛒 <a href="https://apify.com/abotapi/pccomponentes-com-scraper"><b>PcComponentes Scraper</b></a><br>Scrape PcComponentes (pccomponentes.com) products: current price plus strike-through...</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/mediamarkt-es-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 a MediaMarkt category id, with the site's own brand/price filters and native sort. 'url' scrapes any product, category, or search listing page URL you paste, walking pagination forward automatically.

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

Free-text keyword, e.g. 'portatil' or 'cafetera'. Leave empty if you set a Category id below. With no keyword, no Search keywords and no Category id, the run searches the example keyword portatil instead of returning nothing.

## `searchQueries` (type: `array`):

Several keywords in one run; each is searched in turn and results are de-duplicated. Added to Search keyword when both are set. maxItems caps the whole run.

## `categoryId` (type: `string`):

A MediaMarkt category: the numeric id (153), a pasted category URL (mediamarkt.es/es/category/portatiles-153.html), or a plain category name such as 'portatiles' (a plain name is searched as a keyword when the keyword field is empty). 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 MediaMarkt product pages (.../product/\_<slug>-<id>.html), category pages (.../category/<slug>-<id>.html), or search listing pages (.../search.html?query=...). Pagination continues forward automatically from any ?page= already in the URL. Search-mode fields are ignored here. An empty list in URL mode stops the run with an input message. The fields in the Filters section still apply, including to a pasted product URL.

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

Only return products from these brands, e.g. 'APPLE', 'LENOVO', 'ASUS', 'HP'. Match the exact brand name as shown on the site. 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.

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

Only return products rated at or above this many stars, 1-5. 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.

## `condition` (type: `string`):

new or refurbished, decided from the product's own offer (its default offer is the refurbished one, as marketplace refurbished units are), falling back to the site's 'reacondicionado' wording in the name or category path. Applied before rows are counted or billed.

## `sellerType` (type: `string`):

MediaMarkt itself or third-party marketplace sellers. Applied before rows are counted or billed.

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

MediaMarkt's own sort order, verified live by driving the site's sort dropdown -- reorders the full server-side result set, so it stays correct across pagination. 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.

## `sortOrder` (type: `string`):

Alias of Sort order that also accepts lowercase tokens (relevant, price_asc, price_desc, rating, topseller, newest, availability). Sort order wins when both are set.

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

Adds the full flattened spec sheet (varies per category -- RAM/CPU for a laptop, panel type for a TV), delivery/pickup availability detail, energy efficiency label, promo badges, and marketplace/other-seller offers from the product page. Current price, was-price/discount, rating and main image are already included without this toggle.

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

Fetch each product's review summary (average rating, total count) and its most recent reviews (rating, title, text, date, verified-purchaser flag). MediaMarkt renders its newest review page server-side; it does not expose a plain paginated route for a product's entire review history, so this returns that first page, capped by 'Max reviews per product'. Products with no reviews simply return an empty list. The detail-enrichment charge is one unit for the product-page read plus one more for every 10 reviews the product carries (see the Pricing tab).

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

Cap on reviews returned per product when 'Fetch reviews' is on (bounded by what the site's own product page renders, currently up to about 10). 0 = all available on that page. Values above 50 are capped at 50 in code.

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

Stop after this many result pages per search/category/URL entry. 0 = unlimited -- walk every page until the site stops returning new products 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).

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

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

## `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.

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

Remember products between runs and emit only what changed: each row gets changeType NEW, UPDATED, REAPPEARED or EXPIRED, plus changedFields (the names of the fields that differ), firstSeenAt and lastSeenAt. Compared fields: everything in the row except the scrape timestamp, the review list (a new review still shows through rating and reviewCount) and the day-relative delivery fields (deliveryStatus, deliveryDisplayStatus, delivery, fulfillmentEarliest, fulfillmentLatest), which move every day without the product changing; isAvailableForDelivery is still compared. Unchanged products are not emitted or billed unless you turn on the next option. EXPIRED (no longer listed) is only reported after a run that walked the whole result list, so combine with Max products total = 0 and Max pages = 0 for full tracking. Off by default: output is unchanged.

## `stateKey` (type: `string`):

Optional name for the remembered state. Leave empty to derive it from your search, filters and detail/review options, so a different search never shares a baseline. Set the same key on a task to keep one baseline when you edit its search. With Incremental mode on and your own key, search mode needs a keyword, Search keywords or Category id; otherwise the run stops with an input message.

## `emitUnchanged` (type: `boolean`):

Emit products that did not change, with changeType UNCHANGED. Off: they are skipped and not billed.

## `emitExpired` (type: `boolean`):

After a complete run, emit one EXPIRED row for each previously seen product that is no longer in the results. Each emitted row is a normal result row.

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

Field names that must NOT count as a change, for example shippingCost or availabilityStatus. The scrape timestamp, the review list and the day-relative delivery fields (deliveryStatus, deliveryDisplayStatus, delivery, fulfillmentEarliest, fulfillmentLatest) are always ignored. Only used when incremental mode is on.

## `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": "portatil",
  "urls": [
    "https://www.mediamarkt.es/es/category/portatiles-153.html",
    "https://www.mediamarkt.es/es/category/portatiles-153.html?page=2",
    "https://www.mediamarkt.es/es/category/c-153.html",
    "https://www.mediamarkt.es/es/product/_portatil-lenovo-ideapad-slim-3-15arp10-153-wuxga-amd-ryzentm-7-170-16gb-ram-512gb-ssd-radeontm-680m-sin-sistema-operativo-sin-cargador-1625748.html",
    "https://www.mediamarkt.es/es/product/_p-1625748.html",
    "https://www.mediamarkt.es/es/search.html?query=portatil",
    "https://www.mediamarkt.es/es/search.html?query=portatil&page=2"
  ],
  "brands": [],
  "condition": "all",
  "sellerType": "all",
  "sortBy": "RELEVANCE",
  "fetchDetails": false,
  "fetchReviews": true,
  "maxReviewsPerProduct": 20,
  "maxPages": 0,
  "maxItems": 20,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "ES"
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": true,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 50
}
```

# Actor output Schema

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

No description

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

No description

## `reviews` (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": "portatil",
    "urls": [
        "https://www.mediamarkt.es/es/category/portatiles-153.html",
        "https://www.mediamarkt.es/es/category/portatiles-153.html?page=2",
        "https://www.mediamarkt.es/es/category/c-153.html",
        "https://www.mediamarkt.es/es/product/_portatil-lenovo-ideapad-slim-3-15arp10-153-wuxga-amd-ryzentm-7-170-16gb-ram-512gb-ssd-radeontm-680m-sin-sistema-operativo-sin-cargador-1625748.html",
        "https://www.mediamarkt.es/es/product/_p-1625748.html",
        "https://www.mediamarkt.es/es/search.html?query=portatil",
        "https://www.mediamarkt.es/es/search.html?query=portatil&page=2"
    ],
    "brands": [],
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "ES"
    },
    "ignoreFieldsForChanges": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/mediamarkt-es-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": "portatil",
    "urls": [
        "https://www.mediamarkt.es/es/category/portatiles-153.html",
        "https://www.mediamarkt.es/es/category/portatiles-153.html?page=2",
        "https://www.mediamarkt.es/es/category/c-153.html",
        "https://www.mediamarkt.es/es/product/_portatil-lenovo-ideapad-slim-3-15arp10-153-wuxga-amd-ryzentm-7-170-16gb-ram-512gb-ssd-radeontm-680m-sin-sistema-operativo-sin-cargador-1625748.html",
        "https://www.mediamarkt.es/es/product/_p-1625748.html",
        "https://www.mediamarkt.es/es/search.html?query=portatil",
        "https://www.mediamarkt.es/es/search.html?query=portatil&page=2",
    ],
    "brands": [],
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "ES",
    },
    "ignoreFieldsForChanges": [],
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/mediamarkt-es-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": "portatil",
  "urls": [
    "https://www.mediamarkt.es/es/category/portatiles-153.html",
    "https://www.mediamarkt.es/es/category/portatiles-153.html?page=2",
    "https://www.mediamarkt.es/es/category/c-153.html",
    "https://www.mediamarkt.es/es/product/_portatil-lenovo-ideapad-slim-3-15arp10-153-wuxga-amd-ryzentm-7-170-16gb-ram-512gb-ssd-radeontm-680m-sin-sistema-operativo-sin-cargador-1625748.html",
    "https://www.mediamarkt.es/es/product/_p-1625748.html",
    "https://www.mediamarkt.es/es/search.html?query=portatil",
    "https://www.mediamarkt.es/es/search.html?query=portatil&page=2"
  ],
  "brands": [],
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "ES"
  },
  "ignoreFieldsForChanges": []
}' |
apify call abotapi/mediamarkt-es-scraper --silent --output-dataset

```

## MCP server setup

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