By the end of this tutorial, you will have a short Python script that turns an eMAG.ro search or category into structured rows: product name, price in lei as a number, recommended price and discount, rating, review count and stock status.
eMAG.ro is a Romanian online store. It sits behind an AWS WAF captcha: in testing on 2026-10-21, plain, datacenter and Apify Unblocker requests got a captcha page, while Apify residential proxies in Romania reached real pages.
In this guide, we use Python and the Apify Actor eMAG.ro Scraper (reventadata/emag-scraper). It is unofficial and not affiliated with eMAG. You will configure a search, run the Actor and filter rows by price, rating, discount and stock. The row shown below is a sample from a capture dated 2026-10-21, parsed with the Actor's own parser.
⏱️ Time estimate: one sitting: install one package, set one token, run three short Python blocks.
Prerequisites
To follow this tutorial, you will need:
-
Python 3.11+ (
apify-client3.3.0 requires it). - The Apify API client for Python.
- An Apify account and its API token, which sits on the API & Integrations page in Apify Console.
Install the client:
pip install "apify-client~=3.0"
The code reads the token from an environment variable:
export APIFY_API_TOKEN="your_apify_token_here"
What you'll achieve
You will turn an eMAG.ro listing into one row per product. Each row carries the offer id, productId, sku, name and url; price in lei as a number with currency: "RON"; listPrice (the recommended retail price, PRP, printed on the card), previousPrice (a struck-through price, when shown) and discountPercent; rating and reviewCount; availability, inStock and the card's stockLabel; category, categoryTrail, badges, image and position.
Note: The Actor reads listing cards only. Per its README, seller names and full specifications sit on the product page and are not read.
Example use case: Suppose you want iPhones you can buy today at 20% or more under their recommended price. You would set searchQueries: ["iphone"], exactMatchesOnly: true, onlyInStock: true and minDiscountPercent: 20, and every saved row would meet all three conditions.
Step 1: Access eMAG.ro Scraper
Go to the eMAG.ro Scraper page on Apify Store. The README lists every input and output field. In this tutorial we drive the Actor from Python, so everything after this step happens in code.
Step 2: Configure the search
There are two ways to say what to read:
-
searchQueries: one eMAG.ro search per term, for exampletelevizor,aspirator robotoriphone. -
startUrls: listing URLs copied from the site. Onlyhttps://www.emag.ro/search/<term>and category paths ending in/c(for examplehttps://www.emag.ro/laptopuri/c) are accepted; the query string is dropped.
Each result page holds up to 60 products. maxPagesPerListing (1 to 100, default 5) bounds how many pages each term or URL walks, and maxItems (default 100) caps the rows saved across all of them. The site itself serves at most 100 pages, 6,000 products, per listing.
Here is the setup. It searches iphone, reads one result page and saves at most 20 rows:
import os
from apify_client import ApifyClient
# 1. Initialize ApifyClient with the token from the environment
client = ApifyClient(os.environ["APIFY_API_TOKEN"])
# 2. Define the search: one term, one result page, at most 20 rows
run_input = {
"searchQueries": ["iphone"],
"maxItems": 20,
"maxPagesPerListing": 1,
}
💡 Tip: eMAG's search is fuzzy. In the capture used for this post, iphone was answered with the site's "Telefoane Mobile" category, and 3 of the 60 products on page 1 were other phones (a mini XS18 Pro, a Honor 600 Pro and a ZTE nubia). exactMatchesOnly: true keeps only names that contain every word of the term.
No proxy setting is needed in run_input: when none is given, the Actor uses Apify residential proxies in Romania, which reached real pages in testing on 2026-10-21.
Step 3: Add optional filters
| Parameter | Type | What it keeps |
|---|---|---|
sort |
string |
RELEVANCE, PRICE_ASC, PRICE_DESC, REVIEWS, DISCOUNT or NEWEST, applied through the page's own sort link. |
minPrice / maxPrice
|
number | Products inside the band, in lei. The band is also sent to the site through its own price-interval link, so it narrows what the site returns. |
minRating |
number | Average rating at or above the value (0 to 5). Unrated products are dropped when set. |
minDiscountPercent |
integer | At least N% under the struck-through price, or under the PRP when none is shown. Products showing neither are dropped when set. |
onlyInStock |
boolean | Products you can buy now: IN_STOCK, LIMITED_STOCK or IN_STOCK_SUPPLIER. |
exactMatchesOnly |
boolean | For search terms, names containing every word of the term, ignoring case and accents. |
includeKeywords / excludeKeywords
|
array | Words the name or category must / must not contain, ignoring case and accents. |
Three combinations that work:
-
Cheapest TVs in a band:
searchQueries: ["televizor"],sort: "PRICE_ASC",minPrice: 1000,maxPrice: 2000. -
Laptops on sale:
startUrls: ["https://www.emag.ro/laptopuri/c"],sort: "DISCOUNT",onlyInStock: true. -
Well-reviewed iPhones only:
searchQueries: ["iphone"],exactMatchesOnly: true,minRating: 4.5.
⚠️ A
sortoutside the six values is refused before the run starts. AstartUrlsentry that is not a search or category listing (a product page, say, or emag.bg) stops the run with an error at its start.
Step 4: Run the Actor
Now start the run with client.actor("reventadata/emag-scraper").call(run_input=run_input). The call starts the Actor on Apify and waits for it to finish before returning the run:
# 3. Start the Actor run and wait for it to finish
print("Starting eMAG.ro extraction...")
run = client.actor("reventadata/emag-scraper").call(run_input=run_input)
if run is None or run.status != "SUCCEEDED":
status = run.status if run else "unknown"
raise SystemExit(f"Run did not succeed (status: {status}).")
# 4. Fetch the saved products
items = client.dataset(run.default_dataset_id).list_items().items
print(f"Retrieved {len(items)} products.\n")
Behind the scenes:
- The Actor builds the search URL (
/search/iphonehere,/search/iphone/p2for page 2) and fetches each page through an Apify residential proxy in Romania. - It reads the site's own result count on page 1 and walks no further than that count divided by 60, the 100-page ceiling or
maxPagesPerListing. - It parses each product card, skips a product it already saved (the site repeats some products across pages), applies your filters and saves the rows. It stops at
maxItems.
Step 5: View and download results
Loop over the rows to print the parts you need:
for item in items:
print(item["name"])
print(f" Price: {item['price']} {item['currency']}")
if item.get("discountPercent") is not None:
reference = item.get("previousPrice") or item.get("listPrice")
print(f" Discount: {item['discountPercent']}% under {reference}")
print(f" Stock: {item['availability']}")
print(f" Rating: {item.get('rating')} ({item.get('reviewCount')} reviews)")
print("-" * 40)
Here is one row, a sample from a capture dated 2026-10-21 (page 1 of the iphone search) parsed with the Actor's own parser. A saved row also carries scrapedAt, added at save time:
{
"id": "83822499",
"productId": "40684424",
"sku": "DQFCMXMBM",
"name": "Telefon mobil Apple iPhone 13, 128GB, 5G, Midnight",
"url": "https://www.emag.ro/telefon-mobil-apple-iphone-13-128gb-5g-midnight-mlpf3rm-a/pd/DQFCMXMBM/",
"price": 3100.0,
"currency": "RON",
"listPrice": 3863.87,
"previousPrice": null,
"discountPercent": 20,
"rating": 4.81,
"reviewCount": 1101,
"availability": "IN_STOCK",
"inStock": true,
"stockLabel": "în stoc",
"category": "Telefoane Mobile",
"categoryTrail": [
"Laptop, Tablete & Telefoane",
"Telefoane mobile & accesorii",
"Telefoane Mobile"
],
"badges": [
"Top Favorite"
],
"hasGeniusBadge": true,
"image": "https://s13emagst.akamaized.net/products/40685/40684414/images/res_974dc0e4b6a1180b612c24afef8eb494.jpg?width=720&height=720&hash=27A6CA20492D41746C0C8A0226456760",
"position": 26,
"matchedKeyword": "iphone",
"page": 1
}
Apify datasets export as JSON, CSV, XML, Excel, HTML table, RSS or JSONL.
Understanding your results
The fields that matter most:
-
price,currency: the current price in lei as a number (RON). -
listPrice,previousPrice,discountPercent: the PRP printed on the card, the struck-through price when the card shows one, and the drop from the struck-through price, or from the PRP when none is shown. In the 60-product capture, 10 rows had a PRP, none a struck-through price, and their discounts ran from 9% to 33%. -
availability,inStock: one ofIN_STOCK,LIMITED_STOCK,IN_STOCK_SUPPLIER,PREORDER,OUT_OF_STOCK,SHOWROOM_ONLY, orUNKNOWNfor a code not yet understood. The capture had 36IN_STOCK, 6LIMITED_STOCK, 18PREORDERand none in the other states, soinStockwas true on 42. -
rating,reviewCount: null when a product has no reviews; 8 rows in the capture, all pre-orders. -
stockLabel: the card's own text, such asîn stocorultimul produs in stoc(last one in stock); most cards carry none. -
matchedKeyword,page,position: which term or URL, which page and which slot produced the row.
Pro tips
-
Use a price band to go past 6,000 products. A listing stops at 100 pages. Because
minPriceandmaxPriceare also sent to the site, splitting a big category into bands reaches products the unbanded listing never shows. -
Pair price sorting with exact matches. In a capture of
iphonesorted byPRICE_ASC, the first three rows were a Nokia 105, a MaxCom MM142 and a replacement LCD display for an iPhone.exactMatchesOnlydrops the first two;excludeKeywords: ["display"]drops the third. -
Batch terms in one run. Put several terms in
searchQueries. A product kept under one term is not saved again under the next. -
Read stock as data.
onlyInStockkeeps buyable products; for a pre-order tracker, filteravailability == "PREORDER"in your own code instead. -
Start small. Keep
maxPagesPerListing: 1andmaxItems: 20until your downstream code handles every field, then raise them step by step.
Troubleshooting
-
Zero rows: a run that is blocked or cannot read a page ends FAILED; a search that loads normally but matches nothing ends SUCCEEDED with an empty dataset and a status message saying so. Loosen
minRating,minDiscountPercentor the keywords. -
Run stops at once: neither
searchQueriesnorstartUrlswas given, or a URL is not a/search/or/clisting on www.emag.ro. -
Unrelated products: eMAG's search is fuzzy. Add
exactMatchesOnlyorincludeKeywords. -
Fewer rows than expected: read the summary line near the end of the log,
Run finished: N rows pushed. Rejections: ...; it counts each reason a product was dropped (price-band,rating,not-in-stock,discount,not-exact-match,keywords,duplicate,scope-changed). From Python:client.log(run.id).get().
For help, use the Actor's Store page.
Pricing and next steps
For current pricing and details, see the Store page.
To repeat a search, save the input as a task and add an Apify schedule, which runs an Actor or task on a cron expression (0 8 * * * is every day at 8 AM). To react when a run finishes, add a webhook on ACTOR.RUN.SUCCEEDED; its payload carries the run object, including defaultDatasetId.
Which products or categories are you planning to track on eMAG.ro? Drop a comment! 💬
Top comments (0)