By the end of this tutorial, you will have a short Python script that turns a Cash Converters Spain search into structured rows: title, asking price in euros, the shop's own condition grade, the discount badge, the price when new, the saving against new, and the store that holds each unit.
Cash Converters is a second-hand shop chain, not a marketplace: it grades its own stock as Perfecto, Bueno or Usado and writes its own titles. That makes its catalogue a clean reference for second-hand prices.
In this guide, we use Python and the Apify Actor Cash Converters Spain Scraper (reventadata/cashconverters-scraper). The sample row shown below is a sample from a capture dated 2026-08-24 (the day it was added to the Actor's test suite), 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 a Cash Converters Spain search into one row per physical unit, not per model. Each row carries title, price and currency (EUR), condition (the shop's grade), discountPercent, priceWhenNew, savingVsNew, brand, category, productId, storeId, a url that opens this exact unit, and imageUrl.
Note: The same model can appear several times, once per branch that holds a unit. Those are different phones in different stores, not duplicates.
Example use case: Suppose you want the resale price of an iPhone 13 mini 128 GB in good condition or better. You would set searchQueries: ["iphone 13 mini"], conditions: ["a", "b"] and minStorageGb: 128, and every saved row would be a Perfecto or Bueno unit whose title states at least 128 GB.
Step 1: Access Cash Converters Spain Scraper
Go to the Cash Converters 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 search
The main input is searchQueries: one Cash Converters search per term, such as iphone 13 mini or macbook pro. There is no default; a run with no non-empty keyword fails with a Bad input message instead of searching for something you did not ask for.
maxItems caps the rows saved across the whole run (default 1000). Each keyword is paged until the results end, 100 items per request, up to 40 pages.
Here is the setup. It searches iphone 13 mini 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 search: one keyword, at most 10 rows run_input = { "searchQueries": ["iphone 13 mini"], "maxItems": 10, } 💡 Tip: the site's own search is fuzzy: iphone 13 mini also returns iPhone 13 and iPhone 13 Pro. requireKeywordMatch (on by default) drops any item whose title does not contain every word of your query, ignoring case and accents.
No proxy setting is needed in run_input. The README notes the catalogue answers plain HTTP; the input defaults to Apify Proxy, and "proxyConfiguration": {"useApifyProxy": false} fetches directly.
Step 3: Add optional filters
Some inputs are sent to Cash Converters with the search, so the site itself narrows what it returns. Others run on the rows the Actor reads:
| Parameter | Type | Where it applies |
|---|---|---|
conditions | array | Sent to the site: a Perfecto, b Bueno, c Usado. Empty means any grade. One pass per grade. |
minPrice / maxPrice | integer | Sent to the site, in euros. Left out of the request when empty. |
sortBy | string | Sent to the site: price-low-to-high, price-high-to-low, Novedad Tienda Desde (newest in store) or Oferta (on offer). Order only. |
minStorageGb | integer | Row filter on the capacity in the title. 1TB counts as 1024. Titles with no capacity are dropped when set. |
minDiscountPercent | integer | Row filter on the discount badge (0 to 99). Rows with no badge are dropped when set. |
requireKeywordMatch | boolean | Row filter: every query word in the title. Default on. |
fetchDetails | boolean | Not a filter: opens each product page for battery health, stock and rating. |
Three combinations that work:
- Cheapest good units first:
conditions: ["a", "b"],sortBy: "price-low-to-high". - Big discounts only:
minDiscountPercent: 40. - Price band:
minPrice: 150,maxPrice: 400.
⚠️
conditionsaccepts onlya,bandc. Any other value is refused before the run starts, which matters here because Cash Converters answers an unknown grade with an empty result, not an error.
Step 4: Run the Actor
Now start the run with client.actor("reventadata/cashconverters-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 Cash Converters extraction...") run = client.actor("reventadata/cashconverters-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 rows items = client.dataset(run.default_dataset_id).list_items().items print(f"Retrieved {len(items)} items.\n") Behind the scenes:
- The Actor calls the same grid endpoint the site's own "load more" button uses, once per keyword and grade.
- It reads each product tile's data-layer attribute (id, name, brand, category, price, grade), applies your filters and saves the rows.
- It stops at
maxItems, at the end of the results, or at the 40-page cap, which it reports in the log when it bites.
Step 5: View and download results
Loop over the rows to print the parts you need:
for item in items: print(f"{item['title']} ({item.get('condition')})") print(f" Price: {item['price']} EUR") if item.get("priceWhenNew") is not None: print(f" New: {item['priceWhenNew']} EUR, saving {item['savingVsNew']} EUR") print(f" Store: {item.get('storeId')}") print(f" Link: {item.get('url')}") print("-" * 40) Here is one row, a sample from a capture dated 2026-08-24 (the day it was added to the Actor's test suite), parsed with the Actor's own to_row for the keyword iphone 13 mini. Prices and stock may have changed since:
{ "batteryHealth": null, "batteryHealthMin": null, "inStock": null, "ratingValue": null, "title": "apple iphone 13 mini 128gb", "price": 283.95, "currency": "EUR", "condition": "Usado", "discountPercent": 35, "priceWhenNew": 439.99, "savingVsNew": 156.04, "brand": "apple", "category": "teléfonos/móviles/apple/iphone 13 mini 128gb", "productId": "CC007_E1032776_0", "storeId": "CC007", "url": "https://www.cashconverters.es/es/es/comprar/telefonos/moviles/apple/iphone-13-mini-128gb/?firstProduct=CC007_E1032776_0", "imageUrl": "https://images.cashconverters.es/productslive/smartphone/apple-iphone-13-mini-128gb_CC007_E1032776-0_0.jpg?d=small", "matchedKeyword": "iphone 13 mini" } Apify datasets export as JSON, CSV, XML, Excel, HTML table, RSS or JSONL.
Understanding your results
The fields that matter most:
-
priceandcurrency: the asking price in euros, excluding delivery. -
condition: the shop's own grade,Perfecto,BuenoorUsado. On a discounted tile the visible text readsNuevo 439,99 €, whereNuevolabels the new price, not the grade; the Actor reads the grade from the tile's data attribute, so the sample row saysUsado. -
priceWhenNew: the struck-through figure, what the item costs new, not a former asking price.savingVsNewispriceWhenNewminusprice: 439.99 minus 283.95 gives 156.04 in the sample. -
discountPercent: the badge the site shows against the new price, 35 in the sample. -
storeId: the branch holding the unit, from the id prefix.CCcodes are Spanish stores,PTcodes Portuguese stores selling through the same catalogue. -
url: keeps?firstProduct=<id>, so it opens this unit, not just the model page. -
batteryHealth,batteryHealthMin,inStock,ratingValue: null unlessfetchDetailsis on.
Pro tips
- Narrow on the site first.
conditions,minPriceandmaxPricechange what Cash Converters returns;minStorageGbandminDiscountPercentonly drop rows after reading. A narrow row filter on a broad search reads many pages for few rows. - Read battery health with its floor. With
fetchDetails,batteryHealthis a figure for this unit andbatteryHealthMinis the warranty floor, usually 80. In the description "batería al 100% y una vida útil superior al 80%", the 100% is the charge level; the Actor's parser returns no health figure and a floor of 80 for it. - Batch keywords in one run. Put several terms in
searchQueries. A unit saved under one term is not saved again under the next, and a unit rejected under one term can still match a later one. - Mind the grade passes. Each grade in
conditionsis its own pass over the results; leave it empty for any grade in one pass. - Start small. Keep
maxItems: 10until your downstream code handles the fields, then raise it.
Troubleshooting
- Run failed with "Bad input":
searchQuerieshad no non-empty keyword, orminPrice/maxPricewas not a finite number. - Input refused before the run starts: a
conditionsvalue other thana,borc, or asortByvalue outside the list. - Run succeeded with zero rows: the status message starts
0 results for keywords ..., says the pages loaded normally but nothing matched, and lists the rejections. Widen the price range, dropminStorageGborminDiscountPercent, or turn offrequireKeywordMatch. - Run failed with "Every feed failed": no feed returned product tiles, for example a keyword the site has no results for. Check the keyword on the site.
- Fewer rows than expected: read the log's
Done. N item(s) considered, N pushed.line, and look forStopped at the 40-page cap for: ...; narrow the keyword or set a price range to see the rest. 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 models are you planning to track on Cash Converters? Drop a comment! 💬