eBay is a prime target for price tracking, competitor monitoring, and deal sourcing — but its listing pages are heavily A/B-tested, so scraping rendered HTML with CSS selectors breaks constantly. The reliable source is the JSON-LD block eBay embeds in every item page: a <script type="application/ld+json"> tag containing a clean Product object with name, price, currency, and availability. The samples below fetch the page through the Bright Data Web Unlocker and read straight from that structured data.
Prerequisites
export PROXY_URL="http://brd-customer-<id>-zone-<unblocker_zone>:<password>@brd.superproxy.io:22225"
No Bright Data account yet? Get started with the eBay collector →
Items are identified by their numeric item ID from the URL: https://www.ebay.com/itm/<id>.
PHP
<?php
// Run: php ebay.php 167890123456
$proxy = getenv('PROXY_URL');
$itemId = $argv[1] ?? '167890123456';
$ch = curl_init("https://www.ebay.com/itm/$itemId");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_PROXY => $proxy,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_TIMEOUT => 60,
CURLOPT_HTTPHEADER => ['Accept-Language: en-US,en;q=0.9'],
]);
$html = curl_exec($ch);
curl_close($ch);
// eBay ships product data as JSON-LD. Find the block whose @type is "Product".
$doc = new DOMDocument();
@$doc->loadHTML($html);
$xp = new DOMXPath($doc);
$product = null;
foreach ($xp->query('//script[@type="application/ld+json"]') as $node) {
$ld = json_decode($node->textContent, true);
if (($ld['@type'] ?? '') === 'Product') { $product = $ld; break; }
}
$offer = $product['offers'] ?? [];
echo json_encode([
'id' => $itemId,
'name' => $product['name'] ?? null,
'price' => $offer['price'] ?? null,
'currency' => $offer['priceCurrency'] ?? null,
'availability' => $offer['availability'] ?? null,
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), PHP_EOL;
Node.js
// ebay.mjs — node ebay.mjs 167890123456
// Install: npm i axios https-proxy-agent cheerio
import axios from 'axios';
import { HttpsProxyAgent } from 'https-proxy-agent';
import * as cheerio from 'cheerio';
const agent = new HttpsProxyAgent(process.env.PROXY_URL);
const itemId = process.argv[2] ?? '167890123456';
const { data: html } = await axios.get(`https://www.ebay.com/itm/${itemId}`, {
httpsAgent: agent, proxy: false, timeout: 60_000,
headers: { 'Accept-Language': 'en-US,en;q=0.9' },
});
const $ = cheerio.load(html);
let product = {};
$('script[type="application/ld+json"]').each((_, el) => {
try {
const ld = JSON.parse($(el).text());
if (ld['@type'] === 'Product') product = ld;
} catch { /* skip malformed blocks */ }
});
const offer = product.offers ?? {};
console.log(JSON.stringify({
id: itemId,
name: product.name ?? null,
price: offer.price ?? null,
currency: offer.priceCurrency ?? null,
availability: offer.availability ?? null,
}, null, 2));
Rust
// Cargo.toml:
// reqwest = { version = "0.12", features = ["blocking"] }
// scraper = "0.20"
// serde_json = "1"
use scraper::{Html, Selector};
use serde_json::Value;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let item_id = std::env::args().nth(1).unwrap_or_else(|| "167890123456".into());
let client = reqwest::blocking::Client::builder()
.proxy(reqwest::Proxy::all(std::env::var("PROXY_URL")?)?)
.danger_accept_invalid_certs(true)
.build()?;
let html = client
.get(format!("https://www.ebay.com/itm/{item_id}"))
.header("Accept-Language", "en-US,en;q=0.9")
.send()?
.text()?;
let doc = Html::parse_document(&html);
let sel = Selector::parse(r#"script[type="application/ld+json"]"#).unwrap();
let mut product = Value::Null;
for el in doc.select(&sel) {
let raw = el.text().collect::<String>();
if let Ok(ld) = serde_json::from_str::<Value>(&raw) {
if ld["@type"] == "Product" { product = ld; break; }
}
}
let offer = &product["offers"];
let out = serde_json::json!({
"id": item_id,
"name": product["name"],
"price": offer["price"],
"currency": offer["priceCurrency"],
"availability": offer["availability"],
});
println!("{}", serde_json::to_string_pretty(&out)?);
Ok(())
}
What the samples return
We loaded a live eBay listing on 30 October 2026 and read its JSON-LD with the same logic as the samples. The Product block was there, next to a separate ItemPage block that the samples skip.
| Field | Where it comes from | Example from that listing |
|---|---|---|
name | name | Gaming keyboard Wired Gaming Mouse Kit K87… |
price | offers.price | 30.02 |
currency | offers.priceCurrency | USD |
availability | offers.availability | https://schema.org/InStock |
When the fields come back empty
- No
Productblock at all. The page you got is probably a challenge or error page. Print the page title: a verification page means the request was flagged, so route it through the unblocker and slow down. - An ended or removed listing. eBay keeps many ended listings online but may drop the offer, or redirect to a similar item. Store the final URL and treat a missing
offersobject as "not for sale" rather than as a price of zero. - Several blocks. Item pages carry more than one JSON-LD block. Always pick the one whose
@typeisProduct, as the samples do, instead of taking the first.
Notes
- JSON-LD survives eBay's frequent layout experiments far better than CSS selectors. On the listing we checked, the block also carried
image,brand,model,mpnandcolor, and the offer includeditemConditionandshippingDetails. Fields vary by listing, so treat any of them as optional. availabilityis a schema.org URL (e.g.https://schema.org/InStock); strip the prefix if you only want the status word.- Auction listings differ from fixed-price ones — for live bids you may need the bidding section in the DOM rather than JSON-LD.
- For a tracker, persist
{id, price, timestamp}per run and schedule with cron — see the Amazon tracker for the full alerting pattern.
Scaling beyond a few items
Hitting thousands of eBay listings on a schedule means rotating IPs, handling blocks, and absorbing layout changes — maintenance that adds up fast. Bright Data's eBay data collector returns structured listing data without you managing any of that infrastructure, and pre-built datasets cover bulk historical pulls.
Staying within the rules
Collect public product data only, keep request rates modest, and read the site's terms before you build anything commercial on the data. Don't scrape account pages, buyer or seller personal details, or anything behind a login. Our how to avoid getting blocked guide covers polite crawling in more detail.
FAQ
Why is the price missing for some eBay listings?
Listings that have ended, or that are auctions with live bidding, may not carry a usable offers price in their JSON-LD. Store the result as "no current offer" rather than as zero, and for live auctions read the bidding section of the page instead.
What does the availability URL mean?
It's a schema.org value such as https://schema.org/InStock or https://schema.org/OutOfStock. Strip the prefix if you only want the status word.
How often should I check an eBay listing?
Daily is enough for most price tracking. Fixed-price listings rarely change more often than that, and fewer requests mean fewer blocks and less load on eBay.
See our E-commerce Web Scraping Solutions overview, the eBay Product Search Scraping guide for market-level pricing, the Bright Data Web Unlocker review, and How to Avoid Getting Blocked.