CompSniper
Node.js tutorial

How to get eBay sold listings with JavaScript and TypeScript

Use native Node.js fetch to retrieve completed eBay sales, sold prices, dates, conditions, and a cleaned price summary. The examples below cover TypeScript types, eight marketplaces, bounded pagination, and correct handling for rate limits and exhausted quota.

Published September 3, 2026By Marc AndrewTested with Node.js 22
01

Does eBay have a sold-listings API for JavaScript?

eBay’s public Browse API returns active listings, not completed sales. Its Marketplace Insights API covers sold history but is restricted. A practical Node.js integration therefore needs an approved data provider or an independently maintained sold-listings API. CompSniper returns up to 240 completed sales through one authenticated GET request.

This distinction is confirmed in the eBay Developer Support answer and the official Marketplace Insights overview.

02

Make the first sold-listings request with native fetch

Keep the API key on your server. Node.js 20 and newer include fetch, URLSearchParams, and AbortSignal.timeout, so this example needs no HTTP package.

JavaScript
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),
  },
);

if (!response.ok) {
  throw new Error(`CompSniper returned HTTP ${response.status}`);
}

const data = await response.json();
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);
}

Production verification

Tested September 3, 2026 against the live API with Node.js 22. A 10-row Sony WH-1000XM5 request returned HTTP 200 and a $124 median. Treat every response as a current sample, not a guaranteed future selling price.

Editorial illustration of a server-side JavaScript integration transforming completed marketplace listing cards into structured price data and a distribution chart.
A server-side JavaScript request turns completed listing evidence into structured price intelligence.
03

Type the listing rows and price summary

The public response keeps monetary listing values as decimal strings while summary statistics are numbers. Nullable fields reflect information that eBay did not display on that result card.

TypeScript
type SoldListing = {
  itemId: string | null;
  title: string | null;
  soldPrice: string | null;
  soldCurrency: string | null;
  endedAt: string | null;
  condition: string | null;
  shippingPrice: string | null;
  totalPrice: string | null;
  bestOfferAccepted: boolean | null;
  url: string | null;
};

type SoldSearchResponse = {
  keyword: string;
  totalItems: number;
  totalResults: string | null;
  hasNextPage: boolean;
  rawMedian: number | null;
  rawSampleCount: number | null;
  summary: {
    count: number;
    currency: string | null;
    median: number | null;
    mean: number | null;
    p25: number | null;
    p75: number | null;
    avgShipping: number | null;
  };
  items: SoldListing[];
};

const data = (await response.json()) as SoldSearchResponse;
04

Choose the marketplace and narrow the sample

Set ebaySite explicitly when your users sell outside the US. Keep marketplace, condition, and price constraints in request parameters, then use minus-sign exclusions for recurring keyword noise.

UK marketplace
const params = new URLSearchParams({
  keyword: "iphone 15 pro -case -charger",
  ebaySite: "ebay.co.uk",
  count: "100",
  itemCondition: "used",
  minPrice: "250",
  maxPrice: "1200",
  sortOrder: "endedRecently",
});

Eight marketplaces

The response shape stays consistent across US, UK, Germany, France, Italy, Spain, Canada, and Australia.

Raw evidence remains available

Set relevance=false when you intentionally need every valid upstream match.

05

Retry temporary failures, never exhausted quota

HTTP 429 has two meanings. Retry rate_limited after Retry-After, but stop immediately for quota_exceeded. Temporary 500, 502, and 503 responses can use bounded exponential backoff with jitter.

JavaScript retry helper
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function searchWithSafeRetries(params) {
  for (let attempt = 0; attempt < 5; attempt += 1) {
    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 body = await response.json();

    if (response.ok) return body;
    if (response.status === 429 && body.code === "quota_exceeded") {
      throw new Error(`Quota exhausted. Upgrade: ${body.upgrade_url}`);
    }

    const temporary =
      (response.status === 429 && body.code === "rate_limited") ||
      [500, 502, 503].includes(response.status);
    if (!temporary || attempt === 4) {
      throw new Error(body.error || `HTTP ${response.status}`);
    }

    const retryAfter = Number(response.headers.get("Retry-After"));
    const delay = Number.isFinite(retryAfter) ? retryAfter * 1000 : Math.min(1000 * 2 ** attempt, 30_000);
    await sleep(delay + Math.random() * 400);
  }
}

Read X-Usage-Remaining on successful responses and surface the provided upgrade URL before an automated workflow reaches zero.

06

Paginate with a deliberate request ceiling

Each successful page consumes one request. Increment page while hasNextPage is true, but always set a maximum so a scheduled job cannot run forever.

Three pages maximum
const allItems = [];

for (let page = 1; page <= 3; page += 1) {
  const params = new URLSearchParams({
    keyword: "sony wh-1000xm5",
    count: "240",
    page: String(page),
  });
  const data = await searchWithSafeRetries(params);
  allItems.push(...data.items);
  if (!data.hasNextPage) break;
}

console.log("Collected rows:", allItems.length);

Evidence behind the cleanup

What changed across 100 real searches

In CompSniper’s predeclared 100-product study, 19,220 raw priced rows became 11,942 cleaned rows. The median moved by at least 10% for 34 products. Read the method, complete product list, limitations, and aggregate CSV before generalizing those results.

Frequently asked questions

JavaScript and eBay sold data

Does eBay's Browse API return sold listings?

No. eBay Developer Support states that Browse API returns active items, not sold listings. Marketplace Insights supports sold history, but access is restricted. CompSniper provides an independent sold-listings API for developers who need completed-sale data.

Can I call the API directly from browser JavaScript?

Do not expose an API key in browser code. Call CompSniper from a Node.js server, serverless function, background worker, or another trusted backend environment.

How many sold listings can one JavaScript request return?

One request can return up to 240 sold listings. The final number can be smaller when eBay has fewer matches or relevance cleaning removes accessories, parts, and wrong models.

Can Node.js search eBay UK, Germany, Canada, and Australia?

Yes. Set ebaySite to one of eight supported marketplaces: ebay.com, ebay.co.uk, ebay.de, ebay.fr, ebay.it, ebay.es, ebay.ca, or ebay.com.au.

Clone the public examples

Node.js, Python, cURL, pagination, retry, and Card Batch workflows.

Prefer Python?

Use the equivalent production-minded Python tutorial.

Run the JavaScript example with real sold data

Create a free key with 100 monthly requests. No credit card required.