DEV Community

Cover image for How to Scrape Todocoleccion Auctions and Prices with Python
ReventaData
ReventaData

Posted on Originally published at apify.com

How to Scrape Todocoleccion Auctions and Prices with Python

By the end of this tutorial, you will have a short Python script that turns a todocoleccion.net category into structured rows: lot title, link, subcategory, price in euros, whether the lot is an auction, when that auction ends, its bid count, and whether a fixed-price seller accepts offers.

Todocoleccion is a Spanish marketplace for collectibles: watches, coins, stamps, books, antiques. Many of its lots are running auctions, and the site shows an auction's opening bid and a fixed price in the same price element.

Here we use Python and the Apify Actor Todocoleccion Scraper (reventadata/todocoleccion-scraper). You will pick a category, run the Actor and narrow the rows by sale type, price and seller type. The rows shown below are a sample from a capture dated 2026-08-22, 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 todocoleccion.net category into one row per lot. Each row carries the lot id, title and url, the category you asked for, the card's subcategory and subcategoryUrl, price and currency (EUR), isAuction, auctionEndsAt (ISO 8601 with offset), bidCount, acceptsOffers and imageUrl.

Note: The Actor browses categories only. Todocoleccion's robots.txt disallows its free-text search (/buscador*) and explicitly allows paged category pages (/s/*?P*), so there is no keyword input.

Example use case: Suppose you collect watches and want auctions in the watch category with an opening bid of at least 50 euros, cheapest first. You would set categories: ["relojes"], saleType: "auction", minPrice: 50 and sortBy: "priceAsc", and every saved row would be an auction lot priced 50 euros or more.


Step 1: Access Todocoleccion Scraper

Go to the Todocoleccion 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 category slugs, each browsed in turn. A slug is the last part of a category page's address: https://www.todocoleccion.net/s/relojes gives relojes. Subcategories work the same way; the sample row below links to /s/relojes-actuales-varios, which is a slug of its own.

Two inputs bound the run:

  • maxPages: result pages per category. Each page holds 30 lots, and the site serves at most 1000 pages (30,000 lots) for one query, so the input is clamped at 1000 (its default).
  • maxItems: the most rows saved across all categories (default 300).

Here is the setup. It browses relojes, reads one result page 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 input: one category, one result page, at most 10 rows
run_input = {
    "categories": ["relojes"],
    "maxPages": 1,
    "maxItems": 10,
}
Enter fullscreen mode Exit fullscreen mode

💡 Tip: a slug is one path segment. relojes/venta-directa stops the run with an error at its start (the first slug is checked at the start); use saleType: "fixed" instead, and the Actor adds that segment itself.

No proxy setting exists in the input: the Actor always fetches through Apify's residential proxy with a Spanish exit. Requests with no proxy from Apify's servers did not reach the site when the Actor was built.


Step 3: Add optional filters

Parameter Type What it does
saleType string any, fixed or auction. fixed requests the site's own /venta-directa listing; auction drops non-auction rows after fetching.
minPrice / maxPrice integer Whole euros. Sent as the site's preciodesde / preciohasta, and re-checked on every row.
sortBy string default, priceAsc (cheapest first) or priceDesc.
fetchItemDetails boolean Opens each lot page for description, condition, shipping, category path and seller type. Off by default.
sellerType string any, private or professional. Needs fetchItemDetails.

Three combinations that work:

  • Watch auctions from 50 euros up: categories: ["relojes"], saleType: "auction", minPrice: 50, sortBy: "priceAsc".
  • Fixed-price stock only: saleType: "fixed", no price bound.
  • Shops only, with condition text: fetchItemDetails: true, sellerType: "professional".

⚠️ Set both price bounds and only the minimum goes to the site: todocoleccion.net answers HTTP 403 to a request carrying both. The Actor applies the maximum to the rows itself, so the rows you get are the same.

minPrice above maxPrice, or sellerType without fetchItemDetails, stops the run with an error at its start. A saleType, sortBy or sellerType value outside its list is refused before the run starts.


Step 4: Run the Actor

Now start the run with client.actor("reventadata/todocoleccion-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 Todocoleccion extraction...")
run = client.actor("reventadata/todocoleccion-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 lots
items = client.dataset(run.default_dataset_id).list_items().items
print(f"Retrieved {len(items)} lots.\n")
Enter fullscreen mode Exit fullscreen mode

Behind the scenes:

  • The Actor builds the category URL (/s/relojes?P=1 here) and fetches it through the residential proxy.
  • It parses the server-rendered lot cards, applies your filters and saves the rows.
  • It stops at maxItems, at maxPages, at a page that repeats the previous one, or when the site reports 0 lotes for the query.

Step 5: View and download results

Loop over the rows to print the parts you need:

for item in items:
    print(item["title"])
    print(f"  Price: {item['price']} {item['currency']}")
    if item["isAuction"]:
        print(f"  Auction ends: {item['auctionEndsAt']} ({item['bidCount']} bids)")
    else:
        print(f"  Fixed price, accepts offers: {item['acceptsOffers']}")
    print(f"  Subcategory: {item.get('subcategory')}")
    print("-" * 40)
Enter fullscreen mode Exit fullscreen mode

Here are two rows, a sample from a capture dated 2026-08-22 (page 1 of /s/relojes) parsed with the Actor's own parser, one fixed-price lot and one auction:

[
    {
        "id": "703965835",
        "title": "BONITO RELOJ ANALOGICO DE QUARTZ DE SEÑORA GREALY",
        "url": "https://www.todocoleccion.net/relojes/bonito-reloj-analogico-quartz-senora-grealy~x703965835",
        "category": "relojes",
        "subcategory": "Otros relojes de marcas actuales de segunda mano",
        "subcategoryUrl": "https://www.todocoleccion.net/s/relojes-actuales-varios",
        "price": 20.0,
        "currency": "EUR",
        "isAuction": false,
        "auctionEndsAt": null,
        "bidCount": null,
        "acceptsOffers": true,
        "imageUrl": "https://cloud10.todocoleccion.online/relojes/tc/2026/08/22/19/703965835.jpg?size=292x292&crop=true"
    },
    {
        "id": "521038857",
        "title": "Antiguo reloj de sobre mesa despertador swiza mignon",
        "url": "https://www.todocoleccion.net/despertadores-antiguos/antiguo-reloj-sobre-mesa-despertador-swiza-mignon~x521038857",
        "category": "relojes",
        "subcategory": "Relojes despertadores antiguos",
        "subcategoryUrl": "https://www.todocoleccion.net/s/relojes-despertadores",
        "price": 20.0,
        "currency": "EUR",
        "isAuction": true,
        "auctionEndsAt": "2026-09-01T23:00:00+02:00",
        "bidCount": 0,
        "acceptsOffers": false,
        "imageUrl": "https://cloud10.todocoleccion.online/despertadores-antiguos/tc/2024/12/22/12/521038857_tcimg_E227C3F9.jpg?size=292x292&crop=true"
    }
]
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: the asking price on a fixed-price lot, the current or opening bid on an auction lot. The site shows both in the same element, and isAuction tells them apart. In the sample, both lots read 20 euros, one of each kind.
  • isAuction: set from the auction badge on the card. On a sampled unfiltered relojes page in the build notes (2026-08-22), 23 of 30 cards were auctions.
  • auctionEndsAt: the end time as ISO 8601 with its offset. The card's visible countdown (10d 3h on the captured card) is not copied; the Actor takes the seconds remaining that the card carries and adds them to the moment it fetched the page, in UTC.
  • bidCount: from the card's N pujas line; null on fixed-price lots. All 23 auctions on that sampled page read 0 pujas.
  • acceptsOffers: true when a fixed-price card says Admite ofertas.
  • category is the slug you asked for; subcategory and subcategoryUrl are the card's own, narrower link.

With fetchItemDetails on, rows also carry description, condition (the site's sentence verbatim, such as Normal (con señales de uso normal)), shippingFrom (cheapest shipping, euros), categoryPath and sellerType.


Pro tips

  1. Split big categories. The site stops at 1000 pages per query; on the captured page, relojes reported 109,635 lots. Run subcategory slugs, or price bands with minPrice and maxPrice, as separate queries, so each one stays under the 30,000-lot ceiling.
  2. Pair a sort with a price bound. The input description warns that cheapest first fills with 0,01-euro lots and most expensive first with five-figure outliers.
  3. Batch categories in one run. Put several slugs in categories; a lot saved under one is skipped under the next, and the run stops once maxItems is reached.
  4. Turn on details only when you need them. fetchItemDetails adds one request per lot, where one grid page holds 30 lots. A lot page that fails keeps its grid fields.
  5. Professional rows carry the shop. With sellerType: "professional", rows add sellerName, sellerProfileUrl, sellerLocation, sellerRating and three more. Private-seller rows carry sellerType only; the identity fields are absent, not null.

Troubleshooting

  • Run FAILED with zero rows: a run that is blocked or cannot read a page ends FAILED. A wrong slug gets HTTP 404 from the site, logged as a warning for that category before the Actor moves on; with no rows saved, that unreadable page ends the run FAILED. A category that loads normally but matches nothing ends SUCCEEDED with an empty dataset and a status message saying so.
  • Run stops at once: minPrice above maxPrice, sellerType without fetchItemDetails, or a slug containing / (the first slug is checked at the start).
  • Fewer rows than expected: read the log. It prints the site's lot count per category, one Rejected N: reason line per filter (price outside ..., not an auction, seller is not ...) and Pushed N row(s). 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 query, 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 categories are you planning to track on Todocoleccion? Drop a comment! 💬

Top comments (0)