eBay is one of the largest marketplaces on the web, and every public listing carries the kind of structured data that powers price tracking, competitor research, and market analysis: a title, a price, the item condition, shipping cost, the seller, and a link back to the listing. The catch is that eBay renders prices and much of the listing detail with JavaScript and challenges automated traffic hard, so a plain HTTP request hands you a near-empty shell instead of the data you came for.

This guide shows you how to scrape eBay with JavaScript and Node.js the reliable way. You build a small, runnable scraper that fetches a rendered eBay search or listing page through the Crawling API, parses each result with cheerio, and prints a clean structured record per item. We keep the whole walkthrough scoped to public listing data, and the legality section near the end is not boilerplate, so read it before you point this at any real volume.

What you will build

A Node.js script that takes a public eBay search URL, retrieves the rendered HTML through the Crawling API, and extracts a structured record for each listing on the results page. We will use a category search as the running example and pull these fields per item:

  • Title the listing name, for example "Apple iPhone 14 Pro Max 128GB Unlocked".
  • Price the listed price as shown, like "$1,429.49".
  • Condition the item state, for instance "Brand New" or "Pre-Owned".
  • Shipping the shipping cost or "Free shipping" when stated.
  • Seller the seller name or store when present on the card.
  • Item URL the link to the individual listing page.

Why a plain fetch fails on eBay

If you request an eBay search URL with a bare HTTP client, you get a response with status 200 and only part of the listing data in the body. Two things work against you. First, eBay renders prices, shipping, and other listing details in the browser with JavaScript, so the initial HTML is incomplete until the page's scripts run. Second, eBay flags automated traffic quickly: datacenter IPs and request patterns that do not look like a real browser get challenged, rate-limited, or blocked before they ever reach the rendered content.

So a working eBay scraper needs two things in one request: a browser that actually renders the page, and an IP the platform reads as a real visitor. You can assemble that yourself with a headless browser plus a pool of rotating residential proxies, but stitching those together and keeping them healthy is most of the work. The Crawling API folds both into a single call: you send it the URL with a JavaScript token, it renders the page behind a trusted IP, and it returns finished HTML for you to parse.

Why the JS token

Crawlbase offers two token types. The normal token fetches static HTML; the JavaScript (JS) token renders the page in a real browser first. eBay loads key listing fields client-side, so the JS token gives you the most complete page here. Using the normal token can return a partial result with prices or shipping missing, leaving you nothing reliable to parse.

Prerequisites

You need a few things in place before writing any code. None of them take long.

Basic JavaScript and Node.js. You should be comfortable writing and running a Node script and installing packages with npm. If you are new to Node, the official docs and any beginner course will get you to the level this tutorial assumes. For a fuller walkthrough, see our guide on how to build a web scraper with Node.js.

Node.js 16 or later. Confirm your version with node --version. If you do not have it, install it from the Node.js website or through a version manager like nvm.

A Crawlbase account and JS token. Sign up, open your dashboard, and copy your JavaScript (JS) token from the account docs page. Treat the token like a password: it authenticates your requests, so keep it out of version control.

Set up the project

Create a project folder, initialize it, and install the two libraries the scraper needs.

bash
node --version

mkdir ebay-scraper && cd ebay-scraper
npm init -y

npm install crawlbase cheerio

Two dependencies do the work: crawlbase is the official Node client for the Crawling API, and cheerio parses the returned HTML with a jQuery-style API so you can pull out individual fields by CSS selector. If selectors are new to you, the primer on XPath and CSS selectors is a good companion.

Step 1: Fetch the rendered search page

Start by getting the finished page. Import the CrawlingAPI class, initialize it with your JS token, and request the search URL. Checking the status code before you parse keeps failures loud instead of silent.

javascript
const { CrawlingAPI } = require('crawlbase');

const api = new CrawlingAPI({ token: 'YOUR_CRAWLBASE_JS_TOKEN' });

async function crawl(pageUrl) {
  const options = { ajax_wait: 'true', page_wait: 5000 };
  const response = await api.get(pageUrl, options);
  if (response.statusCode === 200) {
    return response.body;
  }
  console.error(`Request failed: ${response.statusCode}`);
  return null;
}

const searchUrl = 'https://www.ebay.com/sch/i.html?_nkw=smartphone';
crawl(searchUrl).then((html) => {
  console.log(html ? html.slice(0, 500) : 'No HTML returned');
});

The two wait options matter for a client-rendered target like this. ajax_wait tells the API to wait for asynchronous content to finish loading, and page_wait holds for a fixed number of milliseconds after load so late-rendering elements appear before the page is captured. Five seconds is a reasonable start; raise it if prices or shipping come back empty. Run the script with node scraper.js and you should see real listing markup, not a stripped-down shell. That confirms rendering works before you write a single selector.

Crawlbase eBay Scraper

eBay needs a rendered page behind a trusted IP, in one call. The Crawling API takes a JS token, runs the page in a real browser, rotates through residential IPs server-side, and hands you finished HTML, so you skip running a headless fleet and a proxy pool yourself. Point it at a public search page on the free tier first.

Step 2: Parse each listing with cheerio

With rendered HTML in hand, load it into cheerio and walk the result cards. eBay lays each search result out in a repeating block, so you select every card, then read title, price, condition, shipping, seller, and the item link from inside it. Reading each field defensively keeps one missing value from crashing the run.

javascript
const cheerio = require('cheerio');

function parseSearch(html) {
  const $ = cheerio.load(html);
  const items = [];

  $('li.s-item').each((_, el) => {
    const card = $(el);
    const title = card.find('.s-item__title').text().trim();
    if (!title || title === 'Shop on eBay') return;

    items.push({
      title,
      price: card.find('.s-item__price').text().trim() || null,
      condition: card.find('.SECONDARY_INFO').text().trim() || null,
      shipping: card.find('.s-item__shipping').text().trim() || null,
      seller: card.find('.s-item__seller-info-text').text().trim() || null,
      itemUrl: card.find('a.s-item__link').attr('href') || null,
    });
  });

  return items;
}

A couple of details keep this resilient. The first card on every eBay results page is a promotional placeholder titled "Shop on eBay", so the early return skips it. Each field falls back to null when the element is missing, which is common since not every listing shows a seller name or a separate shipping line. The item URL is read from the anchor's href rather than its text, so it is handled with attr instead of text.

Selectors drift

eBay's class names (s-item, s-item__price, SECONDARY_INFO, and the rest) change without notice, and they differ between search and individual listing pages. Treat the selectors above as a starting template, not a contract. When a field comes back as null, re-inspect the live page in your browser's dev tools and update the selector. Periodic selector maintenance is normal for any production scraper, not a sign something is broken.

Step 3: Put it together

Now wire the fetch and the parse into one runnable script. Fetch the rendered HTML, hand it to the parser, and print the structured records.

javascript
const { CrawlingAPI } = require('crawlbase');
const cheerio = require('cheerio');

const api = new CrawlingAPI({ token: 'YOUR_CRAWLBASE_JS_TOKEN' });

async function crawl(pageUrl) {
  const options = { ajax_wait: 'true', page_wait: 5000 };
  const response = await api.get(pageUrl, options);
  if (response.statusCode === 200) return response.body;
  console.error(`Request failed: ${response.statusCode}`);
  return null;
}

function parseSearch(html) {
  const $ = cheerio.load(html);
  const items = [];
  $('li.s-item').each((_, el) => {
    const card = $(el);
    const title = card.find('.s-item__title').text().trim();
    if (!title || title === 'Shop on eBay') return;
    items.push({
      title,
      price: card.find('.s-item__price').text().trim() || null,
      condition: card.find('.SECONDARY_INFO').text().trim() || null,
      shipping: card.find('.s-item__shipping').text().trim() || null,
      seller: card.find('.s-item__seller-info-text').text().trim() || null,
      itemUrl: card.find('a.s-item__link').attr('href') || null,
    });
  });
  return items;
}

async function main() {
  const searchUrl = 'https://www.ebay.com/sch/i.html?_nkw=smartphone';
  const html = await crawl(searchUrl);
  if (!html) return;
  const items = parseSearch(html);
  console.log(JSON.stringify(items.slice(0, 3), null, 2));
}

main();

What the output looks like

Run the full script with node scraper.js and you get a clean array of records, one per listing, ready to write to JSON, CSV, or a database.

json
[
  {
    "title": "Apple iPhone 14 Pro Max 128GB Unlocked New Sealed",
    "price": "$1,429.49",
    "condition": "Brand New",
    "shipping": "Free shipping",
    "seller": "beyond_theworld (4,512)",
    "itemUrl": "https://www.ebay.com/itm/354586733872"
  },
  {
    "title": "Apple iPhone X 64GB Unlocked Good Refurbished",
    "price": "$249.99",
    "condition": "Pre-Owned",
    "shipping": "+$12.50 shipping",
    "seller": "thetechout (1,685)",
    "itemUrl": "https://www.ebay.com/itm/393541114176"
  }
]

Loop through result pages

One page of results is a demo; a real job walks the pagination. eBay exposes the page number through the _pgn query parameter, so you can build each page URL in a loop, fetch it through the Crawling API, parse it with the same function, and collect the rows. Because every results page shares the same card structure, the parser you already wrote works across all of them without changes.

javascript
async function scrapePages(keyword, totalPages) {
  const all = [];
  for (let page = 1; page <= totalPages; page++) {
    const url =
      `https://www.ebay.com/sch/i.html?_nkw=${encodeURIComponent(keyword)}&_pgn=${page}`;
    const html = await crawl(url);
    if (html) all.push(...parseSearch(html));
  }
  return all;
}

scrapePages('smartphone', 3).then((rows) => {
  console.log(`Collected ${rows.length} listings`);
});

To enrich each row with full detail (description, every image, the complete shipping table, seller feedback), take the itemUrl from each card and fetch that individual listing page through the same crawl function, then write a small parser for the listing layout. The pattern is identical: render, then parse. For more on rendering-heavy targets, see how to crawl JavaScript websites.

Staying unblocked

Even with rendering handled, eBay watches for scraper-shaped traffic. A few habits keep a run healthy, and they apply to any hard commercial target.

  • Pace your requests. Hammering pages in a tight loop is the fastest way to get throttled. Spread requests out and vary your keywords instead of crawling one path at full speed.
  • Lean on rotation. A pool of residential IPs spreads requests across many real-user addresses so no single one trips a rate limit. The Crawling API handles this for you; if you roll your own stack, this is the part to get right.
  • Read the status codes. A run that starts returning challenges or errors is telling you the current rate or IP tier is no longer enough. Treat that as signal to back off, not noise to ignore.

For the broader playbook, see how to scrape websites without getting blocked and the deeper dive on how to bypass captchas while web scraping. If you would rather route your own traffic through a rotating pool instead of using the managed API, the Smart AI Proxy gives you the same residential IP rotation as a drop-in proxy endpoint. eBay is also a frequent target for broader ecommerce web scraping work, where the same fetch-then-parse pattern carries across sites.

Whether scraping eBay is allowed depends on eBay's terms of service, your jurisdiction, and what you do with the data. eBay's terms restrict automated access, so scraping can run against those terms regardless of how careful your tooling is. None of the code here changes that; it just makes the technical part work. Read eBay's User Agreement and its robots.txt, and treat both as the boundary for what you collect.

A few lines worth holding to. Collect only public listing data: title, price, condition, shipping, the seller name as shown on the card, and the item link that anyone can see without an account. Respect eBay's stated rate expectations and keep your request volume low enough that you are not straining its servers. Avoid personal data, including anything tied to identifiable buyers or sellers beyond what is publicly listed on a results page. If you plan to reuse the data commercially, get permission or an official agreement rather than assuming silence is consent.

For volume or commercial use, eBay offers official APIs, including the Browse API and the Finding API, and those are the right tools when you need large volumes, guaranteed structure, or commercial rights. This guide is deliberately scoped to public listing and search pages because that is the line that keeps the work defensible. It does not cover anything behind a login, buyer or seller personal data, private messages between users, account or order data gated by a sign-in, or any attempt to bypass authentication. If your project needs more than public listings, eBay's official APIs or a data agreement are the correct path, not a cleverer scraper.

Recap

Key takeaways

  • eBay renders listings client-side. A plain fetch returns an incomplete page, so you must render it before you parse it.
  • You need rendering and a trusted IP together. The Crawling API with a JS token does both in one call; ajax_wait and page_wait control how long it waits for content.
  • cheerio does the extraction. Select every s-item card, then map title, price, condition, shipping, seller, and the item URL to current selectors, and expect those selectors to drift.
  • Scale by looping pages. The _pgn parameter walks the result pages, and the same parser works across every page plus sensible pacing.
  • Stay on public data. Respect eBay's ToS and robots.txt, prefer the official Browse or Finding API for volume or commercial use, and never touch logins, personal data, or private messages.

Frequently Asked Questions (FAQs)

Why does a plain fetch return incomplete data from eBay?

Because eBay renders prices, shipping, and much of the listing detail client-side with JavaScript. The initial HTML is partial until the page's scripts run in a browser, so a raw HTTP request returns status 200 with key fields missing or blank. To get a complete page you have to render it first, which is what the Crawling API's JS token handles for you.

Do I need the normal token or the JS token for eBay?

Use the JS token. The normal token fetches static HTML, which on eBay can come back with prices or shipping missing. The JS token renders the page in a real browser before handing back the HTML, so the listing fields are present when cheerio parses them.

My selectors return null. What changed?

Almost certainly eBay's markup. Its s-item card classes, SECONDARY_INFO markers, and section wrappers change without notice, and they differ between search and individual listing pages, so selectors that worked last month can break. Re-inspect a live page in your browser's dev tools and update the selectors. Periodic selector maintenance is normal for any production scraper.

Should I use eBay's official API or scrape the site?

If you need volume, guaranteed structure, or commercial reuse rights, use eBay's official Browse or Finding API. They are built for that and keep you on the right side of eBay's terms. Scraping public listings with the approach in this guide fits smaller, public-data research where no API access is in place, as long as you respect the ToS, robots.txt, and rate limits.

Can I scrape buyer or seller personal data from eBay?

No, and this guide does not cover it. Buyer details, private messages, and account data sit behind a login, so they are not public data. Scraping login-walled content, personal data, or bypassing authentication to reach it is out of scope here and runs against eBay's terms. For sanctioned access the correct route is eBay's official API or a licensing agreement.

How do I avoid getting blocked while scraping eBay?

Keep your per-IP request rate low, vary your keywords instead of looping one path, and route through rotating residential IPs so no single address trips a rate limit. The Crawling API manages rotation and a trusted IP pool for you; if you build your own stack, that is the part to invest in. Watch the status codes and back off when you start seeing challenges.

Start Building

Crawl any site at scale, without fighting infrastructure.

Crawlbase handles proxies, fingerprints, and CAPTCHAs so your team ships data pipelines instead of maintaining crawl plumbing. 1,000 requests free, no card required.

Self-serve · No sales call required · Enterprise crawl volumes available