If you are building a pricing, resale, or inventory tool, active listings answer the wrong question. The price a seller asks for an item is not necessarily the price a buyer paid.
eBay's public Browse API returns active inventory. Marketplace Insights covers sold history, but access is restricted. That leaves many developers maintaining search-page parsers or using a sold-data provider.
This example uses CompSniper because it returns completed listings and a price summary through one GET request.
Disclosure: I am Marc, the owner of CompSniper.
Make one sold-listings request
Node.js 20 and newer already include fetch, URLSearchParams, and request timeouts, so this example does not need an HTTP package.
const params = new URLSearchParams({
keyword: "sony wh-1000xm5",
count: "10",
ebaySite: "ebay.com",
itemCondition: "used",
});
const response = await fetch(
`https://api.compsniper.com/v1/scrape?${params}`,
{
headers: {
Authorization: `Bearer ${process.env.COMPSNIPER_API_KEY}`,
},
signal: AbortSignal.timeout(75_000),
},
);
const data = await response.json();
if (!response.ok) {
throw new Error(data.error ?? `HTTP ${response.status}`);
}
console.log("Listings:", data.totalItems);
console.log("Median:", data.summary.median, data.summary.currency);
console.log("Range:", data.summary.p25, "to", data.summary.p75);
for (const item of data.items.slice(0, 5)) {
console.log(item.title, item.soldPrice, item.endedAt);
}
Keep the API key in a server, worker, or serverless function. Do not place it in browser JavaScript.
Select the buyer's marketplace
The marketplace matters. A UK reseller normally wants UK sold listings and prices in pounds, not US listings in dollars.
const params = new URLSearchParams({
keyword: "iphone 15 pro -case -charger",
ebaySite: "ebay.co.uk",
count: "100",
itemCondition: "used",
minPrice: "250",
maxPrice: "1200",
});
The same response shape works for the US, UK, Germany, France, Italy, Spain, Canada, and Australia.
Treat the two 429 responses differently
This is the integration mistake that creates the most unnecessary failures.
rate_limited is temporary. Wait for the Retry-After value, add a little jitter, and retry a bounded number of times.
quota_exceeded is not temporary. Stop immediately and show the response's reset time or upgrade URL. Retrying it cannot succeed and only produces more 429 responses.
if (response.status === 429 && data.code === "quota_exceeded") {
throw new Error(`Quota exhausted. Upgrade: ${data.upgrade_url}`);
}
if (response.status === 429 && data.code === "rate_limited") {
const seconds = Number(response.headers.get("Retry-After") ?? 1);
await new Promise((resolve) =>
setTimeout(resolve, seconds * 1000 + Math.random() * 400),
);
}
Temporary 500, 502, and 503 responses can use the same bounded backoff approach. Never build an infinite retry loop.
Type the response instead of guessing
The response includes listing rows plus calculated statistics. Listing prices are decimal strings, while the summary values are numbers. Fields can be null when eBay did not display the information on the result card.
A complete TypeScript quick start is available in the public examples repository:
View the TypeScript example on GitHub
The full tutorial also includes response types, bounded pagination, eight-marketplace handling, Best Offer notes, and the limitations of sold-price samples:
Read the complete JavaScript and TypeScript guide
You can create a free key with 100 monthly requests if you want to run the example against current sold listings.
Top comments (0)