DEV Community

Cover image for How to Scrape Cash Converters Spain Prices and Grades with Python
ReventaData
ReventaData

Posted on Originally published at apify.com

How to Scrape Cash Converters Spain Prices and Grades with Python

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:

  1. Python 3.11+ (apify-client 3.3.0 requires it).
  2. The Apify API client for Python.
  3. 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"
Enter fullscreen mode Exit fullscreen mode

The code reads the token from an environment variable:

export APIFY_API_TOKEN="your_apify_token_here"
Enter fullscreen mode Exit fullscreen mode

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,
}
Enter fullscreen mode Exit fullscreen mode

💡 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.

⚠️ conditions accepts only a, b and c. 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")
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

Apify datasets export as JSON, CSV, XML, Excel, HTML table, RSS or JSONL.


Understanding your results

The fields that matter most:

  • price and currency: the asking price in euros, excluding delivery.
  • condition: the shop's own grade, Perfecto, Bueno or Usado. On a discounted tile the visible text reads Nuevo 439,99 €, where Nuevo labels the new price, not the grade; the Actor reads the grade from the tile's data attribute, so the sample row says Usado.
  • priceWhenNew: the struck-through figure, what the item costs new, not a former asking price. savingVsNew is priceWhenNew minus price: 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. CC codes are Spanish stores, PT codes 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 unless fetchDetails is on.

Pro tips

  1. Narrow on the site first. conditions, minPrice and maxPrice change what Cash Converters returns; minStorageGb and minDiscountPercent only drop rows after reading. A narrow row filter on a broad search reads many pages for few rows.
  2. Read battery health with its floor. With fetchDetails, batteryHealth is a figure for this unit and batteryHealthMin is 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.
  3. 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.
  4. Mind the grade passes. Each grade in conditions is its own pass over the results; leave it empty for any grade in one pass.
  5. Start small. Keep maxItems: 10 until your downstream code handles the fields, then raise it.

Troubleshooting

  • Run failed with "Bad input": searchQueries had no non-empty keyword, or minPrice / maxPrice was not a finite number.
  • Input refused before the run starts: a conditions value other than a, b or c, or a sortBy value 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, drop minStorageGb or minDiscountPercent, or turn off requireKeywordMatch.
  • 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 for Stopped 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! 💬

Top comments (0)