# eMAG Products & Price Scraper (RO / BG / HU) (`scrapyx/emag-products-scraper`) Actor

Scrape product listings from eMAG, the largest online marketplace in Romania, Bulgaria and Hungary. Every row carries both prices, the discount, rating, review count, the full category trail, Genius status and all three product IDs. Search by keyword or walk an exact category URL.

- **URL**: https://apify.com/scrapyx/emag-products-scraper.md
- **Developed by:** [Ibnu Adzim](https://apify.com/scrapyx) (community)
- **Categories:** E-commerce, Business, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.05 / 1,000 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

## eMAG Products & Price Scraper (RO / BG / HU)

Scrape product listings from **eMAG** — the largest online marketplace in
Romania, Bulgaria and Hungary.

### What you get

One row per product (`recordType: PRODUCT`):

`productId` · `offerId` · `pnk` · `productName` · `productUrl` ·
`priceValue` · `oldPriceValue` · **`discountPercent`** · `currency` ·
`ratingAverage` · `reviewCount` · `categoryName` · `categoryTrail` ·
`departmentId` · `availabilityCode` · `isGenius` · `hasUnfairPrice` ·
`listingPosition` · `pageNumber` · `searchRank`

Plus a `SEARCH_SUMMARY` per listing and an `ERROR` row per failed input, so
**every input maps to at least one row**.

### Two ways to point at a listing

```json
{ "searchQueries": ["laptop", "casti bluetooth"], "country": "RO" }
```

```json
{ "categoryUrls": ["https://www.emag.ro/laptopuri/c"], "country": "RO" }
```

Both produce identical PRODUCT rows. Use `categoryUrls` when you need
exactness — and read the next section for why that matters.

### The two search traps — read these

**1. A keyword search can redirect into a curated category.**
`/search/laptop` 302s to `/laptopuri/c`. The rows then describe that
*category*, not your keyword, and the count differs. Not an error, but you
should know: every summary row carries `redirectedToCategory` and `finalUrl`.

**2. A keyword eMAG cannot match still returns 60 confident products.**
There is no "no results" page. eMAG fuzzy-matches into *some* department:

| Query | eMAG's own headline |
|---|---|
| `zzqqxxnonsense12345` | `193 de rezultate in Piese auto pentru "zzqqxxnonsense12345"` |
| `qwertyuiopasdfgh999` | `200 de rezultate in Piese auto pentru "qwertyuiopasdfgh999"` |

Zero overlapping product IDs between those two — so it is not one fixed
fallback set, it is genuinely different junk each time, with a confident count
attached. Six "no results" phrasings were searched for in the HTML; none
exists.

There is no gate that can be trusted, so this actor **guesses nothing**. It
emits eMAG's own headline verbatim as `upstreamResultsText`, with the
department parsed out as `matchedDepartment`, and logs it. If your electronics
query landed in car parts, you will see it. `categoryUrls` is the exact
alternative.

### Rate limiting — the thing that will actually bite you

eMAG answers a client that has burst with **HTTP 511** and a 3 KB body, and
the block persists **for minutes** on that exit IP.

It looks like a TLS allowlist at first: an initial ladder had 8 of 9 profiles
on 511 with only `safari18_0` at 200. Re-testing the *same* profile over time
disproved that —

```
t+0s    chrome124 → 200 (1.22 MB)
t+15s   chrome124 → 200 (1.22 MB)
t+60s   chrome124 → 511
t+135s  chrome124 → 511
```

— the ladder had simply been run after a burst. So:

- the defaults here are deliberately slow: **2 concurrent, 1.5 s between
  request starts**
- recovery rotates the **exit IP**, not just the fingerprint — the throttle
  keys on IP
- Residential proxy is the default, and what matters is **pool size**, not
  stealth: a datacenter range exhausts into the same block
- a run that still cannot get through emits a `rate_limited` ERROR row saying
  exactly that, rather than an empty dataset

### Known limits

- **No detail-page pass.** The listing card already carries name, both prices,
  rating, review count, the full category trail and all three ID systems — and
  the detail page sits behind the same per-IP throttle. A detail pass would
  multiply requests against the one resource this target meters, to re-fetch
  most of what you already have.
- **`availabilityCode` is passed through raw** (1/2/3). eMAG does not document
  the mapping publicly and it was not verifiable by observation, so no invented
  labels.
- **Seller name is not on the listing card.** Only the Genius badge and the
  offer ID are.

### Extraction

No JSON blob carries the listing. The single `application/ld+json` block on a
listing page is an `FAQPage`, not products, and `div.card-v2` is an **empty
shimmer placeholder** — reading it returns 78 blank cards. The real data is in
`data-*` attributes on `div.card-item.card-standard` (60 per page,
server-rendered, complete).

Pagination is **honest**: pages 1/2/3/10 share zero product IDs, and past the
end pages shrink then empty rather than re-serving page 1 (`p50` → 60 cards,
`p100` → 18, `p200` → 0, against a category reporting 5,958 products). The
whole catalogue is reachable.

### Policy

`emag.ro/robots.txt` names no AI crawler, and `/search/` and the `/<category>/c`
listing paths are allowed for `User-agent: *`. Only public product listings
are read — no login, no user data, no personal profiles.

# Actor input Schema

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

Keywords to search, one per entry. Read the caveat: eMAG fuzzy-matches, so a keyword it does not recognise still returns 60 confident-looking products from an unrelated department rather than nothing, and a keyword it does recognise may be redirected into a curated category. Every summary row reports which happened via 'matchedDepartment' and 'redirectedToCategory'. Use Category URLs when you need exactness.

## `categoryUrls` (type: `array`):

Exact eMAG category listing URLs to walk, e.g. https://www.emag.ro/laptopuri/c — paste them from your browser. Unlike keyword search these are unambiguous: no fuzzy matching and no redirect. Must be on the same site as the selected country.

## `country` (type: `string`):

Which eMAG market to scrape. The catalogue, prices, availability and currency all differ by market.

## `maxProductsPerListing` (type: `integer`):

Stop after this many products per query or category URL. Pages hold 60 products each and pagination is honest all the way to the end of a category, so large values genuinely work — they just take proportionally longer, because this target must be crawled slowly.

## `maxConcurrency` (type: `integer`):

Upper bound on requests in flight. Kept low on purpose: eMAG's defence is a per-IP rate limit (HTTP 511) that persists for minutes once tripped, so concurrency is the fastest way to break a run. Paging inside one listing is sequential regardless.

## `minRequestInterval` (type: `string`):

Minimum delay between the START of consecutive requests. On this target that is the anti-block measure, not a courtesy: the throttle keys on request rate. Lower it only with a large residential proxy pool, and raise it if you see rate-limit warnings in the log.

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

Proxy used for every request. Residential is the default, pinned to the selected country. What matters here is pool size rather than stealth — eMAG throttles per exit IP, and a datacenter range exhausts into the same block. The country pin is also a correctness setting, since the catalogue and prices are per market.

## Actor input object example

```json
{
  "searchQueries": [
    "laptop"
  ],
  "country": "RO",
  "maxProductsPerListing": 120,
  "maxConcurrency": 2,
  "minRequestInterval": "1.5",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `items` (type: `string`):

One row per scraped record. See the dataset's default view for field definitions.

# 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 = {
    "searchQueries": [
        "laptop"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapyx/emag-products-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 = { "searchQueries": ["laptop"] }

# Run the Actor and wait for it to finish
run = client.actor("scrapyx/emag-products-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 '{
  "searchQueries": [
    "laptop"
  ]
}' |
apify call scrapyx/emag-products-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapyx/emag-products-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/7DwPLiOWuiTlq3dBz/builds/GJfaPIYF0mUifeJHv/openapi.json
