By the end of this tutorial, you will have a short Python script that turns an ebay.es search into structured rows: title, price in EUR, condition, auction state and bid count, shipping cost, ship-from country, and whether the seller is a private person or a business.
eBay Spain puts a JavaScript browser challenge in front of its search pages. In testing on 2026-08-22, plain, datacenter and residential requests all got the same canned error page, while Apify Unblocker returned real result pages.
In this guide, we use Python and the Apify Actor eBay Spain Scraper (reventadata/ebay-scraper). You will configure a search, run the Actor and read the rows. 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 an ebay.es search into one row per active listing. Each row carries the listingId and a clean url, the title, price in EUR, eBay's own condition wording, sellerType (private or business), the auction fields (isAuction, bidCount, timeLeft, endsAt), the shipping fields and shipFrom.
Note: Active listings only. Per the README, sold and completed listings are not available: eBay's sold filters send an anonymous visitor to a sign-in page. The README also states that eBay's robots.txt disallows the
/sch/search path this Actor reads, so you can weigh that yourself.
Example use case: Suppose you want used iPhones from private sellers between 200 and 600 EUR. You would set searchTerms: ["iphone 15 pro"], condition: "used", sellerType: "private", minPrice: 200 and maxPrice: 600.
Step 1: Access eBay Spain Scraper
Go to the eBay 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 searchTerms, and it is required: one or more keywords, typed exactly as you would type them into eBay's own search box. Each term is searched in turn, and every row records the term it came from in keyword.
Two inputs bound the run:
-
maxItems(default 240) caps the rows saved across all terms. -
maxPages(1 to 500, default 500) caps the pages read per term. The Actor already stops at the real end of a query, worked out from eBay's own result count.
itemsPerPage sets how many listings eBay puts on each page: "60", "120" or "240" (the default). Per the input description, 240 makes a quarter of the requests that 60 makes, for the same rows.
Here is the setup. It searches iphone, reads one page of 60 listings 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, one page of 60, at most 10 rows run_input = { "searchTerms": ["iphone"], "itemsPerPage": "60", "maxPages": 1, "maxItems": 10, } No proxy setting exists: the Actor always fetches through Apify Unblocker.
Step 3: Add optional filters
Three filters go to eBay itself and change the results it serves; one runs on the rows after fetching:
| Parameter | Values | Where it runs |
|---|---|---|
condition | any, new, refurbished, used, forParts | eBay's own condition filter |
listingType | any, auction, buyItNow | eBay's own auction / Buy It Now filter |
minPrice / maxPrice | whole euros | eBay's own price filter; omitted when empty |
sellerType | any, private, business | On the rows, from the seller line eBay prints on each card |
Three example combinations:
- Private sellers only:
sellerType: "private", any term. - Live auctions:
listingType: "auction"; each row then carriesbidCount, andtimeLeftwhen eBay prints one. - Refurbished in a price band:
condition: "refurbished",minPrice: 300,maxPrice: 500.
⚠️ A value outside the lists above is refused before the run starts. A
minPriceabovemaxPricestops the run with an error at its start.
Step 4: Run the Actor
Now start the run with client.actor("reventadata/ebay-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 eBay Spain extraction...") run = client.actor("reventadata/ebay-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 listings items = client.dataset(run.default_dataset_id).list_items().items print(f"Retrieved {len(items)} listings.\n") Behind the scenes:
- The Actor builds the ebay.es search URL from your term and filters and fetches each page through Apify Unblocker.
- It reads eBay's results heading first. When eBay found nothing, the page still shows unrelated listings; none of it is collected.
- It parses each listing card, skips eBay's own promo cards, applies
sellerType, and saves the rows. - It stops at
maxItems, atmaxPages, at the last real page from eBay's count, or at a page that adds no new listing.
Step 5: View and download results
Loop over the rows to print the parts you need:
for item in items: kind = "current bid" if item["isAuction"] else "price" print(item["title"]) print(f" {kind}: {item['price']} {item['currency']}") print(f" Condition: {item.get('condition')}") print(f" Seller type: {item.get('sellerType')}") print(f" Shipping: {item.get('shippingText') or 'not shown'}") print(f" Link: {item['url']}") print("-" * 40) Here are two rows, a sample from a capture dated 2026-08-22 (an iphone search) parsed with the Actor's own parser. keyword and page come from the search loop; here they are the values passed to the parser. First, a fixed-price listing from a business seller:
{ "listingId": "147520935178", "title": "Apple Iphone 15 Black 128GB (ohne Simlock)", "url": "https://www.ebay.es/itm/147520935178", "keyword": "iphone", "page": 1, "condition": "Usado", "sellerType": "business", "price": 369.0, "currency": "EUR", "buyItNowPrice": null, "isAuction": false, "bidCount": null, "timeLeft": null, "endsAt": null, "isBuyItNow": true, "acceptsBestOffer": false, "shippingCost": 0.0, "freeShipping": true, "shippingText": "Envío gratis", "shipFrom": "Austria", "energyClass": "B", "sellerUsername": "marketxpres", "sellerFeedbackPercent": 100.0, "sellerFeedbackCount": 169 } Second, an auction from a private seller that also offers Buy It Now:
{ "listingId": "278302959365", "title": "iPhone 17", "url": "https://www.ebay.es/itm/278302959365", "keyword": "iphone", "page": 1, "condition": "Usado", "sellerType": "private", "price": 400.0, "currency": "EUR", "buyItNowPrice": 1000.0, "isAuction": true, "bidCount": 39, "timeLeft": "Quedan 2 d 3 h", "endsAt": "(Lunes, 23:25)", "isBuyItNow": true, "acceptsBestOffer": false, "shippingCost": 15.0, "freeShipping": false, "shippingText": "+ 15,00 EUR de envío", "shipFrom": null, "energyClass": null } Apify datasets export as JSON, CSV, XML, Excel, HTML table, RSS or JSONL.
Understanding your results
The fields that matter most:
-
price: means two things, andisAuctiontells you which. On a fixed-price listing it is the asking price; on an auction it is the current bid. In the sample, 400 EUR is the bid after 39 bids, andbuyItNowPriceholds the seller's fixed 1,000 EUR. -
sellerType:privateorbusiness, from eBay's own card line (Vendedor particular / Vendedor profesional). -
sellerUsername,sellerFeedbackPercent,sellerFeedbackCount: business sellers only. On a private seller's row the keys are absent, not null, as in the auction above. -
condition: eBay's Spanish wording, verbatim (Usado,Reacondicionado,Solo piezas), not mapped to a grade. -
shippingCost,freeShipping,shippingText: a number, a flag and eBay's own phrase.shipFromis filled only when the card saysdesde <country>. -
timeLeftandendsAt: eBay's phrases.endsAtis a weekday and clock time, not a date. -
energyClass: the EU energy label, A to G, not a condition grade.
Pro tips
- Leave
itemsPerPageat 240 for real runs. The same rows come back in a quarter of the requests 60 needs. The 60 above only keeps the first test small. - Filter on eBay first.
condition,listingTypeand the price bounds change what eBay serves.sellerTypeonly drops rows after the fetch, so pair it with a narrow term. - Batch terms in one run. Put several keywords in
searchTerms. A listing saved under one term is not saved again under a later one. - Treat
pricewithisAuction. Before averaging prices, split auctions from fixed-price rows, or usebuyItNowPricewhere an auction has one. - Start small. Keep
maxPages: 1andmaxItems: 10until your downstream code handles the fields, then raise them. - Read the log on a short run. eBay sometimes answers a URL with a full-size page that carries no listing at all. The Actor retries it, and if a page is still skipped after every retry, the log says how many pages were skipped, so a short dataset is never silent. Re-running usually picks those listings up.
Troubleshooting
- Zero rows: a run that is blocked, skips a page after every retry, cannot parse a results page or runs out of time ends FAILED. If pages loaded normally but your filters dropped everything, or eBay's own heading says zero results for the term, the run ends SUCCEEDED with an empty dataset and a status message saying so.
- Run stops at once:
searchTermswas empty, orminPriceis abovemaxPrice. - Fewer rows than expected: read the lines near the end of the log:
Rejected N: seller is not privatecounts rowssellerTypedropped, and a line reports pages skipped after every retry. From Python:client.log(run.id).get(). - No sold prices: not a bug. The Actor covers active listings only.
-
shipFromis often null: normal. eBay printsdesde <country>only on some cards; a domestic listing usually has none.
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.
What are you planning to track on eBay Spain? Drop a comment! 💬
![Why Object.keys(user) Returns string[], Not (keyof User)[]: TypeScript Means It](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2q4w48b9bzs43tynyhsf.png)
