By the end of this tutorial, you will have a short Python script that turns a Back Market Spain category into structured rows: model name, starting price, the price the device cost new, star rating, review count and condition grade.
Back Market sells refurbished electronics, and backmarket.es sits behind Cloudflare Bot Management. In recon on 2026-08-22, requests with no proxy, datacenter and residential proxies got an error body instead of a page; Apify Unblocker returned the page.
In this guide, we use Python and the Apify Actor Back Market Spain Scraper (reventadata/backmarket-scraper). You will pick a category, run the Actor and narrow rows by price and brand. The sample row below is a sample from a capture dated 2026-09-18, 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 one Back Market category page into one row per product model. A card on that page is a model, "iPhone 16", not a merchant offer. Each row carries id, title, url, brand, model, price (the cheapest variant, the site's Desde), priceWhenNew, rating, ratingCount, availableColors, grade, availableStock and warrantyMonths.
Note: Back Market names no merchant on its category or product pages, so rows carry no seller data. The payload holds a
sellerId; the Actor never reads it.
Example use case: Suppose you want refurbished iPhones that start between 300 and 700 EUR. You would put the iPhone category URL in categories, set minPrice: 300 and maxPrice: 700, and every saved row would have a starting price inside that band, with its price new beside it.
Step 1: Access Back Market Spain Scraper
Go to the Back Market Spain 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 category
The one required input is categories: one or more Back Market Spain category URLs, copied from the address bar. A category is a slug and the UUID after it, for example iphone/aabc736a-cb66-4ac0-a3b7-0f449781ed39; a slug alone is an error. The Actor checks every category before its first request.
There is no free-text search input: backmarket.es's robots.txt disallows */search, so the Actor browses categories only.
Each category page holds 31 models. maxPages (1 to 33, default 33) bounds pages per category, and maxItems (default 300) caps rows across all categories.
Here is the setup. It reads one page of the iPhone category and saves at most 10 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 category: one page, at most 10 rows
run_input = {
"categories": [
"https://www.backmarket.es/es-es/l/iphone/aabc736a-cb66-4ac0-a3b7-0f449781ed39"
],
"maxPages": 1,
"maxItems": 10,
}
No proxy setting exists: the Actor always fetches through Apify Unblocker, the one transport that reached the site in recon.
Step 3: Add optional filters
Back Market's own price, storage, model and colour controls write a URL fragment (#price=600&price=900). A fragment never leaves the browser, and the query-string spelling of the same filters is ignored by the server. So the Actor sends only ?p=<n> and applies its filters to the rows it read:
| Parameter | Type | What it keeps |
|---|---|---|
minPrice / maxPrice
|
integer | Rows whose starting price, in whole euros, is inside the bound. |
brands |
array | Rows whose brand matches, using the site's lowercase value such as apple or samsung, case-insensitive. |
fetchProductDetails |
boolean | Not a filter: opens each model's product page for its variant price matrix. Off by default. |
⚠️
minPricegreater thanmaxPricestops the run with an error at its start. A negative price is refused before the run starts.
Step 4: Run the Actor
Start the run with .call(). It 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 Back Market extraction...")
run = client.actor("reventadata/backmarket-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 models
items = client.dataset(run.default_dataset_id).list_items().items
print(f"Retrieved {len(items)} models.\n")
Behind the scenes:
- The Actor builds the category URL with
?p=Nand fetches each page through Apify Unblocker. - It reads the models from the page's embedded
__NUXT_DATA__payload, keeps the 31 grid records and skips the recommendation carousel, applies your filters and saves the rows. - It stops at
maxItems, atmaxPages, or once it has every model the site reports for the category.
Step 5: View and download results
Loop over the rows to print the parts you need:
for item in items:
print(item["title"], "|", item.get("grade"))
print(f" From {item['price']} EUR, new {item['priceWhenNew']} EUR")
if item.get("price") and item.get("priceWhenNew"):
print(f" {1 - item['price'] / item['priceWhenNew']:.0%} below the price new")
print(f" Rating {item.get('rating')} from {item.get('ratingCount')} reviews")
print("-" * 40)
Here is one row, a sample from a capture dated 2026-09-18 (page 1 of the iPhone category) parsed with the Actor's own parser:
{
"id": "5119391",
"title": "iPhone 16",
"url": "https://www.backmarket.es/es-es/p/iphone-16",
"category": "iphone",
"brand": "apple",
"productType": "Smartphone",
"model": "iphone 16",
"variationId": "0e3441cf-37af-429e-973f-218cb3c1fd63",
"price": 584.0,
"priceWhenNew": 1009.0,
"currency": "EUR",
"isStartingFromPrice": true,
"rating": 4.59,
"ratingCount": 14802,
"availableColors": [
"Azul ultramar",
"Blanco",
"Negro",
"Rosa",
"Teal",
"Ultramarine",
"Verde azulado"
],
"imageUrl": "https://d2e6ccujb3mkqf.cloudfront.net/2d99b692-a008-4345-8196-fea4cf5e4096-1_44974392-bda8-41f2-835b-2410a15bd88a.jpg",
"grade": "Excelente",
"availableStock": 29,
"warrantyMonths": 24
}
Apify datasets export as JSON, CSV, XML, Excel, HTML table, RSS or JSONL.
Understanding your results
The fields that matter most:
-
priceandisStartingFromPrice: the cheapest variant of the model, in EUR, not the price of one device. The real price depends on grade, battery, storage, colour and SIM. All 31 captured rows hadisStartingFromPrice: true, and starting prices ran from 81.99 (iPhone 7) to 1,180 (iPhone 17 Pro Max). -
priceWhenNew: what the site says the device cost new, taken from the card. In the capture, the starting price sat between 20% (iPhone 17 Pro Max) and 78% (iPhone 12) below it; that percentage is our arithmetic, not a site field. -
grade,availableStock,warrantyMonths: they describe the listing behind the starting price. Grades are the site's Spanish words: of 31 rows, 17Correcto, 8Muy bueno, 5Excelente, 1Prémium. All 31 had 24 months. -
rating,ratingCount: two decimals out of 5. 30 of 31 were rated, 4.07 to 4.68; iPhone 15 Pro showed 0 with 0 reviews. -
variationId: the priced listing's public UUID; it changes when another offer becomes the cheapest.
Pro tips
-
Use leaf categories. The site serves at most 1,000 models per category: 32 pages of 31 plus a page of 8. Past that it shows
0 productos, even onelectronica, which reported 6,588 on page 1 (2026-09-18). The Actor stops at page 33 and logs the site's count, so for a large category, run its sub-categories. -
Batch categories in one run. Put several URLs in
categories. A model saved under one is not saved again under the next. -
Turn on details only when you need them.
fetchProductDetailsaddscurrentPrice,grades,batteryOptions,storageOptions,colorOptionsandsimOptions, each option with a label, the site's value and a price, at one extra page request per model. A product page that fails keeps its listing row. -
Filters narrow what you receive, not what is read. A tight
maxPriceon a large category still walks its pages; setmaxPagesto bound it. -
Start small. Keep
maxPages: 1andmaxItems: 10until your code handles the fields.
Troubleshooting
-
Zero rows: the status message names the reason. A page that failed or a category that could not be resolved ends the run FAILED. Rows rejected by your filters, or a category the site itself reports as
0 productos, end it SUCCEEDED with an empty dataset. -
Run stops at once: a category is a slug without its UUID, or
minPriceis abovemaxPrice. -
Fewer rows than expected: read the end of the log.
Rejected N: ...lines count each filter's drops, andPushed N row(s).the saved rows. From Python:client.log(run.id).get(). - A large category stops at 1,000: the site's window, not a fault. Use sub-categories.
For help, use the Actor's Store page.
Pricing and next steps
For current pricing and details, see the Store page.
To track prices, 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 refurbished categories are you planning to track on Back Market? Drop a comment! 💬
Top comments (0)