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:
-
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 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,
}
💡 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")
Behind the scenes:
- The Actor builds the category URL (
/s/relojes?P=1here) 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, atmaxPages, at a page that repeats the previous one, or when the site reports0 lotesfor 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)
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"
}
]
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, andisAuctiontells 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 unfilteredrelojespage 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 3hon 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'sN pujasline; null on fixed-price lots. All 23 auctions on that sampled page read0 pujas. -
acceptsOffers: true when a fixed-price card saysAdmite ofertas. -
categoryis the slug you asked for;subcategoryandsubcategoryUrlare 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
-
Split big categories. The site stops at 1000 pages per query; on the captured page,
relojesreported 109,635 lots. Run subcategory slugs, or price bands withminPriceandmaxPrice, as separate queries, so each one stays under the 30,000-lot ceiling. - 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.
-
Batch categories in one run. Put several slugs in
categories; a lot saved under one is skipped under the next, and the run stops oncemaxItemsis reached. -
Turn on details only when you need them.
fetchItemDetailsadds one request per lot, where one grid page holds 30 lots. A lot page that fails keeps its grid fields. -
Professional rows carry the shop. With
sellerType: "professional", rows addsellerName,sellerProfileUrl,sellerLocation,sellerRatingand three more. Private-seller rows carrysellerTypeonly; 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 runFAILED. A category that loads normally but matches nothing endsSUCCEEDEDwith an empty dataset and a status message saying so. -
Run stops at once:
minPriceabovemaxPrice,sellerTypewithoutfetchItemDetails, 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: reasonline per filter (price outside ...,not an auction,seller is not ...) andPushed 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)