kestrel actors docs

kestrel actors › Ecommerce

eBay Scraper — Search, Listings, Prices & Sellers

An eBay scraper that turns search results and item pages into clean rows: price, struck-through list price, shipping cost, condition, buying format, bids and time left, units sold, watchers, seller feedback score and percentage, item location and image. No API key, no login, no browser. It reads the same public pages a shopper sees, on 18 marketplaces, and bills you per listing row it actually delivers. Use it for ebay price research, for bulk ebay product data, and as an ebay api alternative when the official Browse API's onboarding is more than your project needs.

Run eBay Scraper on Apify →

eBay scraper pricing: what one row costs

EventWhat it isPriceCharged for
itemListing row$0.003One eBay listing from a search page or an item page: price, list price and discount, shipping, condition, buying format, bids and time left, units sold, watchers, seller feedback and location. Searches that return nothing, rows dropped by your price bounds and listings already delivered earlier in the run are all free.

Pay per event: $0.003 per listing row, and nothing else. There is no per-run start fee and no compute charge on top.

One page of 60 listings is $0.18. A daily two-page watch on five keywords is about $1.80 a month.

eBay scraper input reference

Every field of eBay Search & Listings, straight from the actor's input schema.

FieldTypeDefaultWhat it does
queriesarray[]Keywords to search, exactly as a shopper would type them: "mechanical keyboard", "vintage pocket watch". Each is searched across the pages set below, on the marketplace you choose.
startUrlsarray[]eBay search URLs (https://www.ebay.com/sch/i.html?_nkw=...) to read exactly as pasted — every filter already in the URL is kept — and/or individual item URLs (https://www.ebay.com/itm/123456789012), which return the richer item-page row. The marketplace is taken from each URL, so you can mix countries in one run.
domainstring"com"eBay marketplace for the keyword searches: com, de, fr, it, es, at, nl, pl, com.au, com.sg, com.my. These are the hosts measured answering reliably on 2026-08-29; eBay UK, Ireland, Canada, Switzerland and Hong Kong currently return an anti-bot challenge on every proxy pool, so they are not offered here — paste their URLs into the field above if you want to try anyway. URLs bring their own marketplace.
buyingFormatstring (enum)"all"Restrict to Buy It Now listings or to auctions. Auction cards carry the bid count and the time left; fixed-price cards carry units sold and watchers. One of: all, buy_it_now, auction.
conditionsarray[]Keep only these conditions. eBay grades refurbished in four separate tiers, so pick the ones you actually accept. Leave empty for every condition.
minPriceinteger0Lower price bound in the marketplace's own currency. Sent to eBay as _udlo so the results arrive already filtered, then checked again before billing — you are never charged for a listing outside your range. Range 0-10000000.
maxPriceinteger0Upper price bound in the marketplace's own currency. Sent to eBay as _udhi and checked again before billing. Leave at 0 for no upper bound. Range 0-10000000.
categorystring""Optional eBay category id to search inside, e.g. "175672" for computer components. It is the _sacat number in any eBay category URL. Leave empty to search all of eBay.
sortBystring (enum)"best_match"How eBay orders the results. Price sorts are on price plus shipping, which is what a shopper actually compares. One of: best_match, ending_soonest, newly_listed, price_low, price_high.
pagesinteger1How many result pages to walk. Each page carries 60 listings, and eBay stops paginating a search at about 2000 items however many it reports. Range 1-34.
maxResultsPerQueryinteger00 = everything the pages returned. N = the first N in eBay's order. The main cost control: the run stops fetching once each job has N. Range 0-2000.
sessionsinteger4How many proxy sessions (egress IPs) run in parallel. More is faster; each is paced separately and warmed on eBay's homepage before its first request. Range 1-16.
perIpnumber0.5Pace for each session. eBay tolerates about 0.5/s per IP; faster earns interstitials, not speed. Range 0.1-2.
proxyConfigurationobject{"useApifyProxy": true}Apify Proxy is required: eBay answers unproxied and unrotated callers with an Akamai interstitial. The run starts on datacentre proxies and escalates to RESIDENTIAL by itself when eBay pushes back.

Example input

{
  "queries": ["mechanical keyboard", "keycap set"],
  "domain": "com",
  "pages": 2,
  "maxResultsPerQuery": 60
}
{
  "startUrls": [
    "https://www.ebay.co.uk/sch/i.html?_nkw=lego+star+wars&_sacat=19006",
    "https://www.ebay.de/sch/i.html?_nkw=nintendo+switch+oled"
  ],
  "pages": 3
}

eBay scraper output fields

Two row types share one dataset: item (one eBay listing from a search page or an item page, charged) and status (one per keyword or URL, free).

Dataset views in the Apify Console: Overview, Demand signals, Sellers, Price research, Item pages, Run status.

FieldTypeWhat it holds
typestringRow type: item or status.
item_idstringeBay's numeric listing id — the number in any /itm/ URL.
titlestringListing title as the seller wrote it.
urlstringCanonical item page URL.
querystringSearch rows: the keyword that returned this listing.
search_urlstringSearch rows: the exact eBay URL this page was read from, filters included.
pageintegerWhich results page the row came from.
positionintegerThe slot in eBay's own result order, counted across pages.
domainstringMarketplace domain suffix, e.g. com or co.uk.
marketplacestringMarketplace country name.
pricenumberPrice as a number. For a price range, the low bound — what a shopper compares on. Null when eBay withholds the price (a MAP-restricted listing shows "See price" until checkout); price_display then carries eBay's own wording, and a run with price bounds set drops those rows free rather than billing them.
price_displaystringPrice exactly as eBay printed it, range and currency prefix included.
currencystringCurrency read from the rendered price, not assumed from the marketplace.
list_pricenumberStruck-through original price when eBay shows one.
list_price_displaystringOriginal price exactly as printed.
discount_pctnumberHow far below the list price the current price sits, in percent.
shipping_costnumberShipping charge as a number; 0 when delivery is free.
shipping_displaystringShipping line exactly as printed, delivery estimate included.
free_shippingbooleanTrue when eBay shows free delivery.
conditionstringCondition label: Brand New, Pre-Owned, Open Box, Certified - Refurbished and so on.
buying_formatstringbuy_it_now or auction.
bidsintegerAuction rows: how many bids have been placed.
time_leftstringAuction rows: eBay's own time-left text, e.g. "9m" or "1d 4h".
best_offerbooleanTrue when the seller accepts offers.
units_soldintegerHow many units this listing has sold — the closest public signal to real demand.
watchersintegerHow many shoppers are watching this listing.
returnsstringReturns line when eBay shows one, e.g. "Free returns" or "30 days returns".
sellerstringSeller username or store name.
seller_feedback_pctnumberSeller's positive-feedback percentage.
seller_feedback_countintegerSeller's feedback score, expanded from eBay's abbreviated form (40.6K -> 40600).
locationstringWhere the item ships from.
imagestringListing image URL.
ratingnumberProduct star rating out of 5, when the listing is matched to an eBay catalogue product.
ratings_countintegerHow many product ratings that catalogue product has.
results_totalintegerHow many listings eBay reports for the whole search.
quantity_availablestringItem-page rows: availability text, e.g. "Last one" or "More than 10 available".
breadcrumbsarrayItem-page rows: eBay's category path for this listing.
categorystringItem-page rows: the leaf category from the breadcrumb.
item_specificsobjectItem-page rows: the seller's specifics table — brand, model, MPN, colour, size and the rest.
seller_urlstringItem-page rows: the seller's eBay store or profile URL.
statusstringStatus rows: ok, no_results or error.
targetstringStatus rows: the keyword or URL this job was given.
kindstringStatus rows: search, url or item.
itemsintegerStatus rows: listing rows delivered by this job.
filteredintegerStatus rows: rows dropped by your price bounds, never charged.
duplicatesintegerStatus rows: listings another job in the run already delivered.
pagesintegerStatus rows: how many pages were requested.
errorstringStatus rows: why the job failed, null when it did not. An eBay refusal lands here, never as an empty result.
fetched_atstringUTC timestamp when the row was fetched.

eBay scraper in Python, JavaScript and curl

Python

from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("kestrel/ebay-search-scraper").call(run_input={
    "queries": ["mechanical keyboard"],
    "pages": 2,
    "minPrice": 30,
    "maxPrice": 200,
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    if row["type"] == "item":
        print(row["price"], row["currency"], row["units_sold"], row["title"])

JavaScript (Node)

import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('kestrel/ebay-search-scraper').call({
    queries: ['keycap set'],
    pages: 1,
    sortBy: 'price_low',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.filter((r) => r.type === 'item').length, 'listings');

curl

curl -X POST "https://api.apify.com/v2/acts/kestrel~ebay-search-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'content-type: application/json' \
  -d '{"queries":["vintage pocket watch"],"buyingFormat":"auction","pages":1}'

The same three calls are runnable files in the examples repository: examples/ebay-search-scraper.py|.js|.sh.

Limits and honest notes

  • No sold or completed listings. eBay's LH_Sold=1&LH_Complete=1 view is behind a sign-in gate: a logged-out request is redirected to signin.ebay.<tld>. Verified on 2026-08-29 across seven marketplaces, datacentre and residential proxies in seven countries, eight URL routes and three browser fingerprints — not one of roughly 115 attempts returned sold results. A headless browser does not change it, because the block is an HTTP redirect rather than a JavaScript challenge. The honest substitute is units_sold, the units a live listing has already sold at its current price.
  • Sixty listings per page, which is eBay's own ceiling (_ipg=60), and eBay stops paginating a single search at roughly 2,000 items whatever results_total reports — which is why pages is capped at 34.
  • There is no sponsored column. eBay prints the Sponsored disclosure on every card — 60 of 60 on four searches across two marketplaces and both buying formats — so a column that is always true was left out and the finding documented instead.
  • Auction cards carry no seller line on the results page, so those rows carry the seller from the item page only.

FAQ

How many listings are on each eBay page?

Sixty. The actor requests _ipg=60, which is eBay's ceiling. pages: 3 therefore reads up to 180 listings per job. eBay stops paginating any single search at roughly 2000 items however many it reports in results_total, which is why pages is capped at 34.

Why did my search return no rows?

Either eBay genuinely found nothing, or the run was refused. The two are never confused: a genuine miss produces a status row with status: "no_results" and costs nothing, and a refusal produces status: "error" with the reason. This matters more than it sounds — eBay's zero-result page still renders about twenty "results matching fewer words" cards from other searches, and a scraper that delivers those bills you for listings you never asked for. This one drops them.

Do I need a proxy?

Yes, and Apify Proxy is the default. eBay is behind Akamai Bot Manager and refuses unrotated callers with a /splashui/challenge interstitial. The run opens several proxy sessions, warms each one against the marketplace homepage, paces every session separately, retries on a different IP whenever a page is refused, and escalates from datacentre to residential proxies by itself once eBay has pushed back twice. Measured at one request per second on 2026-08-29: datacentre answered 5 of 10 searches and residential 8 of 10, so with retries a run completes reliably on either.

Are sponsored listings flagged?

There is deliberately no sponsored column. eBay's current results page prints the (deliberately obfuscated) "Sponsored" disclosure on every card — 60 of 60 on four different searches across two marketplaces and both buying formats, all from the same organic results module. A column that is always true is worse than no column, so we left it out and documented the finding instead.

Can I track prices over time?

Yes — schedule the run and keep the datasets. Each row carries fetched_at, item_id and price, which is everything a time series needs. A daily run of the same keywords, appended to one table and grouped by item_id, gives you an ebay price tracker with the listing's units_sold and watchers moving alongside the price, which is far more informative than the price alone.

What about eBay Kleinanzeigen or eBay Motors?

Kleinanzeigen is a separate site with its own markup and is not covered. Motors listings are ordinary eBay listings and come back like any other, though vehicle-specific attributes live in item_specifics on item-page rows rather than in dedicated columns.

Is scraping eBay legal?

This actor reads only publicly available pages — the same search results and item pages any visitor sees without logging in — and collects no personal data beyond the seller usernames and store names eBay itself publishes next to every listing. Public-data scraping of this kind is broadly lawful in the US and the EU, and courts have repeatedly declined to treat access to a public web page as unauthorised access. That is not the whole story, though: eBay's own Terms of Service restrict automated access, so scraping can be a contractual matter between you and eBay even where it is not a legal one. You are responsible for your own use — respect robots directives and rate limits, do not republish listing content wholesale, keep collection proportionate to your purpose, and take your own legal advice for anything commercial. We do not scrape anything behind a login, and we will not: that is exactly why the sold-listings view above is absent rather than faked.

How do I find an eBay category id?

Open any eBay category in a browser and read the _sacat number out of the URL, or click into a category from a search and copy it from the address bar. Pass it as category and every keyword in the run is searched inside that category only.

How to use eBay Scraper

Run eBay Scraper

The actor lives on the Apify Store, where the pricing tab carries the live per-row price and the input form has every field above: apify.com/kestrel/ebay-search-scraper