# eBay Scraper - Listings, Sellers & Sold Price Check (`henrycrawl/ebay-listings-scraper`) Actor

Track competitor prices on eBay without the eBay API. Scrape any search into typed rows: price with currency, sold count, condition, shipping, seller username and feedback. Then pass item IDs to checkItems to see which ones sold, at what price and on what date. Hard cap on results.

- **URL**: https://apify.com/henrycrawl/ebay-listings-scraper.md
- **Developed by:** [Henry Crawl](https://apify.com/henrycrawl) (community)
- **Categories:** E-commerce, Lead generation
- **Features:** Limited permissions
- **Stats:** 6 total users, 1 monthly users, 34.5% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.50 / 1,000 listings

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## eBay Scraper - Listings, Sellers, Feedback & Sold Count

**Track competitor prices on eBay without the eBay API.** Point it at any eBay search,
category page or filtered listing URL and get typed rows back — no developer account, no
OAuth, no rate-limit paperwork.

Every row carries the **seller username, feedback percentage and feedback count** that most
eBay scrapers leave empty, prices already parsed into numbers with a currency code, the
`sold` counter where eBay shows one, and a hard cap on how many listings a run may return so
the bill cannot surprise you.

Common uses: repricing against competitors, tracking a category's price band over time,
building a supplier or seller shortlist, and feeding price data into a dashboard.

### What you get per listing

| Field | Example | Notes |
|---|---|---|
| `itemId` | `197740124597` | Stable eBay id, use it to deduplicate across runs |
| `title` | `YETI Rambler 20 oz Tumbler…` | |
| `priceRaw` | `$35.00` | Exactly as eBay rendered it |
| `priceValue` | `35` | Parsed number, ready to filter and aggregate |
| `priceMax` | `48.5` | Filled when the listing shows a price range |
| `currency` | `USD` | Derived from the rendered price; `null` rather than guessed when the symbol is ambiguous |
| `currencyMismatch` | `false` | `true` when the row is not in the currency this marketplace quotes in |
| `expectedCurrency` | `USD` | What `ebaySite` should be quoting, so you can audit the row yourself |
| `condition` | `Brand New` | |
| `sellerName` | `oliviabrian1234` | |
| `sellerFeedbackPercent` | `100` | |
| `sellerFeedbackCount` | `489` | `6K` and `94.2K` are expanded to real numbers |
| `soldCount` | `461` | Present on the listings where eBay shows it |
| `watchers` | `60` | |
| `bidCount` | `3` | Auctions only |
| `shippingRaw` | `+$28.60 delivery` | |
| `shippingValue` | `28.6` | `0` when shipping is free |
| `freeShipping` | `false` | |
| `itemLocation` | `United States` | Where the item ships from |
| `imageUrl`, `itemUrl` | | |
| `pageNumber`, `searchKeyword`, `inputUrl` | | Provenance of every row |
| `ebaySite`, `proxyCountry`, `scrapedAt` | | The exact context the data was captured in |

### Two ways to search

**1. Build a search from filters** — no URL needed. Add one or more keywords; each
runs as its own search with the same filters:

```json
{
  "keywords": ["yeti rambler 20 oz tumbler", "stanley quencher 40 oz"],
  "ebaySite": "www.ebay.com",
  "proxyCountry": "US",
  "condition": "new",
  "buyingFormat": "buy_it_now",
  "minPrice": 20,
  "maxPrice": 60,
  "sortBy": "price_lowest",
  "maxListings": 2000
}
```

**2. Paste URLs** — any eBay search, category or filtered listing page:

```json
{
  "startUrls": [
    "https://www.ebay.com/sch/i.html?_nkw=yeti+rambler&_sacat=0&rt=nc&LH_BIN=1"
  ],
  "proxyCountry": "US",
  "maxListings": 500
}
```

Filters available: eBay site, keyword exclusions, category, min/max price, condition,
buying format, free shipping, and the same sort options as eBay's own dropdown.

### Sold prices: ask the listing, not the search

eBay puts sold *search* behind sign-in — signed out, `…&LH_Sold=1&LH_Complete=1` redirects to
`signin.ebay.com/...&sgfl=srch`. Getting past that would mean running automated searches
through a real eBay account, and the account, not the scraper, is what eBay suspends. This
actor does not go there.

An individual **listing page** is a different surface, and it is open: signed out it answers
normally, eBay's robots.txt allows `/itm/<id>`, and the sale is stated in plain text. So pass
`checkItems` the listings you care about and get the real numbers:

```json
{
  "checkItems": [
    "225076150467",
    "https://www.ebay.com/itm/223223631102",
    "398249946847"
  ],
  "proxyCountry": "US"
}
```

```
223223631102  SOLD                     AU $9.98    sold 2026-07-09T06:19:17.000Z
225076150467  SOLD                     C $40.00    sold 2026-07-12T01:24:17.000Z
398249946847  ACTIVE                   US $100.00
186082326577  ENDED_NOT_SOLD           US $9.99
117340485562  ENDED_PRICE_UNAVAILABLE
```

`saleStatus` is the field to trust, and it has five values:

| Value | Meaning |
|---|---|
| `SOLD` | It sold. `soldAt` carries the exact timestamp, `price` what it went for |
| `ACTIVE` | Still listed; `price` is the current asking price |
| `ENDED_NOT_SOLD` | The seller pulled it. **Not** a sale |
| `ENDED_NO_SALE` | An auction closed with no bids |
| `ENDED_PRICE_UNAVAILABLE` | Ended auction on a catalogue-matched item — see the limit below |

**Why not just read `availability`?** Because eBay marks a pulled listing `OutOfStock` exactly
like a sold one. Treating that as a sale quietly turns unsold stock into fake comps, and
nothing downstream can spot the error. The sentence eBay prints is the only field that
separates them, so that is what `saleStatus` is built from.

#### The honest limits

- **There is no keyword search for sold items here.** That is the part eBay gates. You supply
  the listings — usually item IDs a previous search run returned, re-checked days later.
- **Ended auctions can lose their final price.** When the item matches an eBay catalogue
  product, eBay redirects the ended listing to that catalogue page, which does not show the
  winning bid. Those rows come back `ENDED_PRICE_UNAVAILABLE` with no price rather than
  carrying the catalogue's asking price as if it were a sale.
- **One page per row.** A search page yields up to 240 listings; a listing page yields one. Set
  `maxListings` accordingly — it caps this mode too.
- `checkItems` ignores `keywords` and `startUrls`; run the search separately.

**Still free, still public:** `soldCount` on active listings — the "X sold" figure eBay prints
— needs no item check at all and comes with every search row.

### The run fails if it is served the wrong country

eBay prices by the country of the requesting IP, and a proxy occasionally lands somewhere
other than the one you asked for. When that happens nothing about the rows looks wrong —
right shape, right fields, plausible numbers — only the currency gives it away. Measured
2026-08-28 on a run that asked for `proxyCountry: US`: all 25 rows came back in Brazilian
reais, and an earlier build labelled every one of them `USD`.

So each row now carries `currencyMismatch` and `expectedCurrency`, and a run where more
than half the rows are mismatched **fails** rather than handing you a clean-looking
dataset priced in a market you did not ask for.

### Proxy country matters more than you would expect

eBay localises listing pages: **prices, currency, shipping costs, and even which
listings appear depend on the country of the IP making the request.** Requesting
`www.ebay.com` from outside the US can return prices in a different currency
entirely. Set `proxyCountry` to the market you care about — it defaults to `US`.

`ebaySite` and `proxyCountry` are independent: `ebaySite` picks which regional eBay
to load, `proxyCountry` decides which market you appear to be shopping from.

Residential proxies are used by default. eBay refuses datacenter IP ranges, so a
datacenter group will simply produce a failed run.

### The result cap is real

`maxListings` stops the run at exactly that number. Scrapers that treat their limits
as suggestions produce surprise bills; this one exits as soon as the cap is met, and
also stops on its own if three consecutive pages return nothing usable.

### Runs fail loudly, not silently

Every run writes a `DIAGNOSTICS` record to the key-value store with the block rate,
bytes per item, and the fill rate of every single output field:

```json
{
  "itemsExtracted": 200,
  "blockRate": "0%",
  "mbPer1000Items": 19.06,
  "fieldCoverage": { "priceValue": "100%", "sellerName": "92%", "currency": "100%" }
}
```

If nothing was extracted, or more than half the pages were blocked, the run is marked
**failed** with the reason. A dataset full of empty columns is worse than an error,
because the problem only shows up after you have used the data.

### Typical uses

- Competitor and marketplace price monitoring, with the seller behind each price
- Turnover research using `soldCount` on live listings
- Tracking which sellers list a given brand, and at what price
- Building product and pricing datasets for internal tools or dashboards

### Notes and limits

- `soldCount` and `watchers` appear only on the listings where eBay displays them;
  they are reported as `null` elsewhere rather than guessed.
- eBay places a promotional label in the footer of every result card, organic ones
  included, so this actor does not claim to separate promoted listings from organic
  ones. It does drop the "Shop on eBay" placeholder cards, which carry no data.
- Keep `maxConcurrency` low. eBay authorises by session, so parallelism burns
  sessions faster than it saves time.

### Support

Found a field that stopped filling, or need a filter that is not exposed yet? Open an
issue on the actor page with the input you used — layout changes on eBay are normal
and get fixed.

### FAQ

**Do I need an eBay API key or developer account?**
No. This reads eBay's public search and category pages, so there is nothing to register for
and no OAuth flow. You only need an Apify account.

**How do I get eBay sold prices without the API?**
Read the listing page instead of the search. An individual `/itm/<id>` page is public — it
answers a signed-out request normally, and eBay's robots.txt allows that path — and it states
the sale outright: "This listing sold on Sat, Jul 11 at 6:24 PM." Pass those item IDs to
`checkItems` and you get the sold price, the exact ISO timestamp and the seller, with no key,
no OAuth and no account. What stays out of reach is *searching* sold items by keyword; that
particular page is the one eBay puts behind sign-in.

**I applied for eBay's Marketplace Insights API and was denied. What are the alternatives?**
That API returns 90 days of sold data but is Limited Release, and denials are the norm rather
than the exception — developers with apps in production for years and thousands of users report
being turned down, including ones already in the eBay Partner Network. Rather than reapply,
two public sources cover most of what it was wanted for: `soldCount` on active listings (the
"X sold" figure eBay prints itself) and `checkItems` on specific listings, which returns the
actual sale price and date from the listing page.

**How can I tell whether an eBay listing ended because it sold or because the seller pulled it?**
Not from the availability field, which is the usual mistake — eBay marks both cases
`OutOfStock`. The difference is the sentence eBay prints on the page, and this actor reads it:
`This listing sold on …` becomes `saleStatus: SOLD`, while `This listing was ended by the
seller on … because …` becomes `ENDED_NOT_SOLD`, and an auction that closed without bids
becomes `ENDED_NO_SALE`. Treating "ended" as "sold" is what quietly fills a comps table with
prices nobody ever paid.

**Can I get eBay sold data without logging in?**
For specific listings, yes — listing pages are served to signed-out visitors, and that is all
`checkItems` uses. For keyword searches of sold items, no: signed out, a sold search redirects
to `signin.ebay.com`. The workarounds circulating for that involve driving automated searches
through a real eBay account, and it is the account that gets suspended, so this actor does not
implement them.

**Can it get sold prices and completed listings?**
Yes for specific listings, no for keyword searches. Pass `checkItems` a list of item IDs or
`/itm/` URLs and each row comes back with `saleStatus`, the actual sold price and the exact
sale timestamp, read from the public listing page — no eBay account or cookies involved. What
it cannot do is *search* for sold items by keyword: that surface is behind eBay's sign-in wall,
and the only way through is to automate someone's real account, which risks that account.

**How do I get the item IDs to check?**
From a search run of this same actor. Scrape a keyword today, keep the `itemId` column, then
feed those IDs to `checkItems` a week later — the ones that come back `SOLD` give you the price
and the date they sold at, which is the turnover picture sold comps are usually bought for.

**Does an ended listing always mean it sold?**
No, and this is where sold data usually goes wrong. eBay marks a listing the seller pulled as
`OutOfStock`, exactly like one that sold. This actor reads the status sentence instead, so a
pulled listing returns `ENDED_NOT_SOLD` and never enters your dataset as a sale.

**Why do my prices come back in the wrong currency?**
eBay prices by IP country. Requested from outside the US, `ebay.com` can answer in the local
currency with local delivery. Set `proxyCountry` to match the marketplace you actually want,
and check the `currency` field on the rows rather than assuming.

**How do I avoid a surprise bill?**
`maxListings` is a hard stop, not a suggestion. The run also fails loudly rather than
returning half a dataset: zero items, or a block rate above 50%, ends the run with an error
so you never pay for a quietly incomplete scrape.

**What happens when eBay blocks a request?**
The session is retired and the request retried on a fresh one. Every run reports its block
rate, bytes per item and per-field coverage in the log, so you can see the quality of the
data instead of guessing.

**How do I get eBay sold data?**
Three routes exist and only one is open to most people. eBay's Marketplace Insights API returns
90 days of sold data but is Limited Release, and denials are routine — developers with apps in
production for years report being turned down. Terapeak shows the same data in a browser but has
no public API behind it. The third route is the listing page itself: `/itm/<id>` is served to
signed-out visitors, eBay's robots.txt allows it, and it states the outcome in plain text along
with an exact timestamp. That is what `checkItems` reads. You supply the item IDs — typically
ones a search run of this actor returned earlier — and you get back the real sale price and date.

**How do I find eBay sold prices for items I am tracking?**
Run a search for the products you care about and keep the `itemId` column, then pass those IDs
to `checkItems` a few days or weeks later. Listings that sold come back `saleStatus: SOLD` with
the price they went for and an exact `soldAt` timestamp; ones still running come back `ACTIVE`
with the current asking price. Repeating that on a schedule is what turns a list of listings
into a price history, and none of it needs an eBay account.

**Is there an eBay sold listings API I can call?**
Not a public one from eBay. This actor is the closest practical substitute for the item-level
case: give it eBay item IDs and it returns typed rows — `saleStatus`, `price`, `currency`,
`soldAt`, `sellerName`, `quantitySold` — over Apify's normal API, so you can call it from any
language. The honest gap versus the official API is search: eBay gates keyword search of sold
items behind sign-in, so this cannot find sold items by keyword, only report on listings you
already have IDs for.

**How do I see eBay sold history without a browser extension?**
The sold-history extensions people use are reading the same public pages, one at a time, in your
browser. This does it server-side and in bulk, and returns typed rows instead of a rendered
page, which is the difference that matters if you want the numbers in a spreadsheet or a
database rather than on screen.

**How do I build an eBay price history for a product?**
By snapshotting it yourself, because eBay does not publish one. Run a search for the product on a
schedule and keep every row — each carries `priceValue`, `currency`, `soldCount` and `scrapedAt`,
so a week of runs is already a price series with the seller behind each point. Where it gets more
useful is combining that with `checkItems`: feed back the `itemId` values from earlier runs and
the listings that have since sold return `SOLD` with the price they actually went for and an
exact timestamp, which turns an asking-price series into a record of what the market paid.

### Related scrapers

Same approach applied elsewhere — typed rows, a cap that is actually enforced, and a
diagnostics record on every run:

- [eBay Seller Scraper](https://apify.com/henrycrawl/ebay-seller-scraper) — the companion to this one: point it at a seller instead of a search.
- [Amazon Product Scraper](https://apify.com/henrycrawl/amazon-products-scraper) — prices, ASIN and demand signals, with every row checked against the currency you asked for.
- [Indeed Jobs Scraper](https://apify.com/henrycrawl/indeed-jobs-scraper) — absolute posting dates rather than relative labels.
- [LinkedIn Jobs Scraper](https://apify.com/henrycrawl/linkedin-jobs-scraper) — no login, no cookie, and cheap to run because it needs no browser.

# Actor input Schema

## `keywords` (type: `array`):

Each keyword runs as its own search using the filters below. Leave empty if you paste URLs instead.

## `startUrls` (type: `array`):

Paste eBay search, category or filtered listing URLs. Scraped in addition to any keywords.

## `checkItems` (type: `array`):

eBay item IDs or /itm/ URLs. Instead of searching, the run opens each listing page and reports whether it SOLD, what it actually sold for, and the exact timestamp. This reads public listing pages, so no eBay account or cookies are involved. Note eBay keeps sold *search* behind sign-in, so there is no keyword search for sold items here — you supply the listings, typically ones a previous search run returned. Ended auctions on catalogue-matched items are flagged ENDED_PRICE_UNAVAILABLE because eBay redirects them to a catalogue page that hides the final bid.

## `ebaySite` (type: `string`):

Which regional eBay to search. Independent from Proxy country.

## `proxyCountry` (type: `string`):

eBay localizes prices, currency, shipping costs and even which listings appear, based on the IP's country. Requesting ebay.com from outside the US can return a different currency entirely. Set this to the market you actually care about.

## `maxListings` (type: `integer`):

The run stops at exactly this many listings. Enforced, not a suggestion, so you cannot be billed for more rows than you asked for. Default is deliberately small so a first run is cheap and fast; raise it for real harvests.

## `sortBy` (type: `string`):

Same sort options as eBay's own dropdown.

## `condition` (type: `string`):

Filter by item condition, same values eBay offers.

## `buyingFormat` (type: `string`):

Restrict to Buy It Now or auction listings only.

## `minPrice` (type: `integer`):

Lowest price to include, in the currency of the selected eBay site.

## `maxPrice` (type: `integer`):

Highest price to include, in the currency of the selected eBay site.

## `freeShipping` (type: `boolean`):

Only listings that ship for free.

## `excludeWords` (type: `string`):

Comma or space separated. Applied as eBay negative keywords.

## `categoryId` (type: `string`):

Optional. Narrows the search to one eBay category.

## `excludeSponsored` (type: `boolean`):

Removes eBay's 'Shop on eBay' promo cards, which carry no real listing data. Note: eBay puts a reversed 'Sponsored' string in every card footer, so it cannot be used to tell ads from real listings, and this option does not claim to filter promoted listings.

## `itemsPerPage` (type: `integer`):

eBay supports 60, 120 or 240. Use 240: about 4x fewer requests per 1,000 rows, so lower proxy cost. Lower it only if you hit blocks.

## `maxConcurrency` (type: `integer`):

Keep this low. eBay gates access by session, so hammering it burns sessions faster than it saves time.

## `debugRawText` (type: `boolean`):

Adds the raw text of each result card to the output. Useful when a field stops filling and you want to see what eBay changed.

## `proxyConfiguration` (type: `object`):

Defaults to Apify residential proxy in the selected country. eBay refuses datacenter IP ranges, so a datacenter group will produce a failed run.

## Actor input object example

```json
{
  "keywords": [
    "yeti rambler 20 oz tumbler"
  ],
  "startUrls": [],
  "checkItems": [],
  "ebaySite": "www.ebay.com",
  "proxyCountry": "US",
  "maxListings": 100,
  "sortBy": "best_match",
  "condition": "any",
  "buyingFormat": "any",
  "freeShipping": false,
  "excludeSponsored": true,
  "itemsPerPage": 240,
  "maxConcurrency": 2,
  "debugRawText": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `listings` (type: `string`):

One row per eBay listing: title, parsed price and currency, condition, seller username with feedback percent and count, sold count, shipping, item location, image and listing URL.

## `diagnostics` (type: `string`):

Block rate, pages processed, bytes per item and the fill rate of every output field for this run. Use it to verify the dataset is complete before relying on it.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "keywords": [
        "yeti rambler 20 oz tumbler"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("henrycrawl/ebay-listings-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "keywords": ["yeti rambler 20 oz tumbler"] }

# Run the Actor and wait for it to finish
run = client.actor("henrycrawl/ebay-listings-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "keywords": [
    "yeti rambler 20 oz tumbler"
  ]
}' |
apify call henrycrawl/ebay-listings-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,henrycrawl/ebay-listings-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/oqbTbbsFbbEVb7aR7/builds/Inq79S7APEQwL1PLs/openapi.json
