How to Scrape StockX Data (Prices, Bids, Asks)
Learn how to scrape StockX data at the size level: pull product variants, live lowest ask and highest bid, and sales history past PerimeterX and Akamai.

StockX is one of the harder e-commerce targets on the public web. Try to scrape StockX data with a plain requests.get() and you get an HTTP 403 in well under a second, served by PerimeterX before your code ever sees a price. This guide takes the read-only market-data route. You will find the JSON that StockX's own pages call, read the lowest ask, highest bid, and last sale for every size, pull the recent sales history, and run all of it through a real browser on residential IPs so the bot defense treats you like a shopper checking prices. No buying, no checkout bots, just market data.
What You Can Scrape from StockX
StockX is a bid-ask marketplace, not a fixed-price store. That changes what "product data" even means. A single sneaker is one catalog product with many size variants, and each size trades at its own price with its own buyers and sellers. The data worth collecting splits into four buckets.
| Data category | Where it lives | Key fields |
|---|---|---|
| Catalog / product | `Product` object | `title`, `brand`, `styleId`, `colorway`, `releaseDate`, `retailPrice` |
| Market (overall) | `Product.market` | `lowestAsk`, `highestBid`, `lastSale`, `salesLast72Hours`, `deadstockSold` |
| Variants (per size) | `Product.children[uuid]` | `shoeSize`, `market.lowestAsk`, `market.highestBid`, `uuid` |
| Sales history | `/activity` and `/chart` endpoints | `amount`, `createdAt`, `shoeSize`, price time series |
One thing to settle early: StockX does not publish a general market-data API for the public. Approved sellers get a Seller API for managing their own catalog and listings, but there is no sanctioned feed that hands you live asks and bids for any product you name. The public market data comes from the same JSON the site's own front end reads on every product page. That is what we target here, and we target it read-only.
Is It Legal to Scrape StockX?
Be honest with yourself before you write a line of code. StockX's Terms of Use prohibit automated access and scraping, the same as almost every large marketplace. That does not make the data secret (anyone can see a price without logging in), but it does mean scraping sits in a gray zone that depends on jurisdiction, what you collect, and what you do with it.
A few rules keep this defensible as market research rather than abuse:
- Collect only public data: prices, sizes, and sales counts you can see without an account. Never touch login-gated pages, buyer or seller identities, or anything personal.
- Respect
robots.txtand throttle hard. StockX rate-limits aggressively, and a polite crawler that pauses between requests is both kinder and harder to block. - Keep it read-only. Do not place bids, do not list, do not check out.
That last point draws a clean line. This guide is not a checkout bot. If automated buying is what you are after, that is a separate topic with its own legal exposure, covered in our piece on sneaker botting and retail automation. Reading market prices for research and running a purchase bot are not the same activity, and courts treat them differently. If you plan to build a commercial product on this data, talk to a lawyer first.
Scraping at scale? Skip the blocks.
Fast, unblockable datacentre proxies with unlimited bandwidth.
Why StockX Is a Hard Target
Most sneaker resale sites lean on the same defenses, and StockX runs several at once. Knowing which is which tells you what your request has to look like.
| Defense | Signal you will see | What passes it |
|---|---|---|
| PerimeterX / HUMAN | `_px3`, `_pxvid` cookies; press-and-hold captcha; HTTP 403 | Real browser plus residential IP plus a sticky session |
| Akamai Bot Manager | `_abck`, `bm_sz`, `ak_bmsc` cookies | Consistent TLS plus browser sensor data |
| TLS / JA3 fingerprint | Instant 403 on raw HTTP clients | Chromium via `render_js`, not a bare socket |
| IP reputation | Datacenter ranges blocked early | Residential IPs via `premium_proxy` |
PerimeterX (now branded HUMAN Bot Defender) is the main gate. It fingerprints your TLS handshake, your JavaScript environment, and your mouse behavior, then decides in milliseconds whether to serve the page or a press-and-hold challenge. A bare Python request fails the TLS check alone, which is why the 403 arrives so fast. Our deeper walkthrough on how to bypass PerimeterX covers the challenge mechanics.
The IP dimension matters just as much. StockX scores datacenter ASNs low and blocks them early, so the same request that fails from an AWS box succeeds from a residential address. That is the practical reason this job needs residential rather than datacenter proxies. Two things together get you in: a browser fingerprint PerimeterX accepts, and an IP its reputation model trusts.
Find the StockX Product JSON
You do not parse StockX's HTML. The page is a React front end that hydrates from JSON, and rendering markup would be slow and brittle. Go straight to the data feed.
- Open a product page in Chrome, for example
https://stockx.com/air-jordan-1-retro-high-og-chicago-reimagined. - Open DevTools with
F12, click the Network tab, then the Fetch/XHR filter. - Reload. Watch for a request to
/api/products/.... That is the market feed.
The endpoint follows a predictable shape:
https://stockx.com/api/products/{urlKey}?includes=market¤cy=USD&country=US
The urlKey is the slug in the page URL. Three query parameters carry more weight than they look:
includes=marketis the switch that attaches the live market block. Drop it and you get catalog fields only, no asks or bids.currencyandcountrylocalize the prices. AlowestAskinUSD/USis a different number than the same shoe inEUR/DE, because StockX runs a separate regional market. Pin both so your data set stays consistent.
There is a second route worth knowing. StockX also embeds a hydration payload in the page under a tag, so you can read product data straight from the HTML without a second API call:
import json, re
html = fetch("https://stockx.com/air-jordan-1-retro-high-og-chicago-reimagined")
m = re.search(r'<script id="__NEXT_DATA__"[^>]*>(.*?)</script>', html, re.S)
data = json.loads(m.group(1))
# server-rendered product props live under data["props"]["pageProps"]
Endpoints and field names on StockX drift over time, so treat DevTools as the source of truth and re-check the exact call before a big run. The general technique of replaying a site's own XHR is covered in scraping hidden JSON API endpoints.
Inside the StockX Product JSON
Here is the shape you will see, trimmed to the fields that matter:
{
"Product": {
"uuid": "b0f0b6a2-...-9c1e",
"urlKey": "air-jordan-1-retro-high-og-chicago-reimagined",
"title": "Jordan 1 Retro High OG Chicago Reimagined",
"brand": "Jordan",
"styleId": "DZ5485-612",
"retailPrice": 180,
"market": {
"lowestAsk": 245,
"highestBid": 210,
"lastSale": 232,
"salesLast72Hours": 41,
"deadstockSold": 12873,
"annualHigh": 410,
"annualLow": 178,
"pricePremium": 0.29,
"volatility": 0.11
},
"children": {
"1a2b-...": { "uuid": "1a2b-...", "shoeSize": "10.5",
"market": { "lowestAsk": 249, "highestBid": 205, "lastSale": 236 } }
}
}
}
The market block is the heart of it. Read each field for what it actually tells you:
| Field | Meaning |
|---|---|
| `lowestAsk` | Cheapest price a seller will accept right now |
| `highestBid` | Most any buyer currently offers |
| `lastSale` | Price the most recent trade cleared at |
| `salesLast72Hours` | Trade count over the trailing 72 hours (a liquidity signal) |
| `deadstockSold` | All-time units sold on StockX |
| `annualHigh` / `annualLow` | 52-week price range |
| `pricePremium` | `lastSale` over retail, as a ratio |
| `volatility` | Recent price dispersion |
Do not confuse the three price fields. lowestAsk and highestBid are the current edges of the order book. lastSale is the price the market actually cleared at. If you want a single "market price," lastSale is usually the honest one, and the gap between highestBid and lowestAsk (the bid-ask spread) tells you how liquid the size is. A tight spread with high salesLast72Hours means an efficient market. A wide spread means thin trading, and the sticker price there is soft.
Enumerate Sizes and Variants
This is where most StockX tutorials stop short, and it is the part that carries the real value. The top-level market is only a summary. It reports the best ask and bid across every size, so it hides the fact that a size 10.5 might trade at double the price of a size 14. The per-size numbers live in children, a map keyed by each variant's uuid, and every child has its own market.
sizes = {}
for child in product.get("children", {}).values():
size = child.get("shoeSize")
cm = child.get("market", {})
sizes[size] = {
"lowest_ask": cm.get("lowestAsk"),
"highest_bid": cm.get("highestBid"),
"last_sale": cm.get("lastSale"),
"uuid": child.get("uuid"),
}
for size, row in sorted(sizes.items(), key=lambda kv: float(kv[0] or 0)):
print(size, row["lowest_ask"], row["highest_bid"], row["last_sale"])
Keep each child's uuid. You need it later to pull size-specific sales history, and it is the only stable handle for a variant across runs. If your goal is pricing signal, the size-level table is the data set you actually want, not the single headline number.
Read Market Prices: Lowest Ask, Highest Bid, Last Sale
Now the request itself. You cannot hit that JSON URL with plain requests, because PerimeterX 403s it instantly:
import requests
r = requests.get("https://stockx.com/api/products/nike-dunk-low-retro-white-black-panda?includes=market")
print(r.status_code) # 403, PerimeterX blocks the raw request in milliseconds
Route it through the SparkProxy Scraping API instead. A real Chromium browser handles the fingerprint, premium_proxy puts you on a residential IP, and stealth adds anti-detection layers on top.
import json, re, requests
API = "https://scrape.sparkproxy.io/api/v1"
API_KEY = "YOUR_API_KEY"
def fetch(url, session_id="stockx-1", **extra):
params = {
"url": url,
"render_js": "true", # PerimeterX needs a real browser
"premium_proxy": "true", # residential pool, not datacenter
"stealth": "true", # extra anti-detection layers
"country_code": "US", # match the currency/country in the URL
"session_id": session_id # one browser profile, keeps the _px cookie
}
params.update(extra)
r = requests.get(API, headers={"X-API-Key": API_KEY}, params=params, timeout=90)
r.raise_for_status()
print("credits:", r.headers.get("X-Credits-Used"))
return r.text
def as_json(body):
# Chromium wraps a raw JSON document in minimal HTML; grab the payload.
m = re.search(r"\{.*\}", body, re.S)
if not m:
raise ValueError("no JSON found (likely a PerimeterX block page)")
return json.loads(m.group(0))
With the helpers in place, reading the overall market is a few lines:
url_key = "nike-dunk-low-retro-white-black-panda"
api = f"https://stockx.com/api/products/{url_key}?includes=market¤cy=USD&country=US"
product = as_json(fetch(api))["Product"]
m = product["market"]
print(product["title"], product.get("styleId"))
print("lowest ask :", m["lowestAsk"])
print("highest bid:", m["highestBid"])
print("last sale :", m["lastSale"])
print("spread :", m["lowestAsk"] - m["highestBid"])
Every parameter earns its place. Here is why each one is set the way it is:
| Parameter | Value for StockX | Why it is there |
|---|---|---|
| `render_js` | `true` | PerimeterX rejects anything without a browser fingerprint |
| `premium_proxy` | `true` | Residential IPs; StockX pre-flags datacenter ASNs |
| `stealth` | `true` | Adds fingerprint hardening beyond the base browser |
| `country_code` | `US` | Must match the `currency` and `country` in the API URL |
| `session_id` | stable label | Keeps one browser profile so the `_px` cookie survives |
Cost is worth watching. Residential plus JavaScript rendering plus stealth sits at the high end of the credit table, so cache aggressively and only re-fetch products whose prices you expect to have moved. Set json_response=true if you want SparkProxy's metadata envelope, and read X-Credits-Used from the response headers to track spend per call.
Pull Sales History and Price Trends
Live asks and bids are a snapshot. The history is where the story is, and StockX exposes two feeds for it, both keyed by the product uuid you already captured.
The activity feed returns individual recent trades:
uuid = product["uuid"]
act_url = (
f"https://stockx.com/api/products/{uuid}/activity"
"?state=480¤cy=USD&limit=100&page=1&sort=createdAt&order=DESC&country=US"
)
sales = as_json(fetch(act_url)).get("ProductActivity", [])
for s in sales[:10]:
print(s["createdAt"], s["shoeSize"], s["amount"])
state=480 filters to completed sales. Page through with page and limit to walk back in time. To get sales history for one specific size instead of the whole product, swap in that variant's child uuid, the same value you stored in the size table earlier.
The chart feed returns a smoothed price time series, handy for trend lines:
chart_url = (
f"https://stockx.com/api/products/{uuid}/chart"
"?start_date=2025-01-01&end_date=2025-12-31&intervals=100&format=highstock¤cy=USD"
)
series = as_json(fetch(chart_url)).get("series", [])
points = series[0]["data"] if series else []
print(len(points), "price points") # each point is [timestamp_ms, price]
Combine the two and you can reconstruct a full picture: what the market cleared at recently, how often it trades, and how the price has drifted over months. That is a genuine data set, not a single scraped number.
Scrape StockX Data at Scale with the SparkProxy Scraping API
One product is easy. A watchlist of a few hundred is where blocks show up, and the fix is session discipline.
The _px cookie PerimeterX issues is tied to a browser profile. If every request spins up a fresh profile, PerimeterX re-challenges each time, which is slow and block-prone. Reuse a session_id and the cookie carries across requests. When a session does get flagged, rotate to a new one rather than hammering the same profile. A two-step warm-up helps for a cold session: load the product page first to mint the cookie, then hit the API on the same session.
# 1) warm the session by loading a real page (mints the _px cookie)
fetch(f"https://stockx.com/{url_key}", session_id="stockx-batch-A")
# 2) reuse the SAME session_id so the cookie and profile carry over
product = as_json(fetch(api, session_id="stockx-batch-A"))["Product"]
For a full run, spread work across a handful of sticky sessions, pause between requests, and write straight to CSV:
import csv, time
url_keys = ["nike-dunk-low-retro-white-black-panda", "adidas-samba-og-white-black"]
with open("stockx.csv", "w", newline="") as f:
w = csv.writer(f)
w.writerow(["url_key", "size", "lowest_ask", "highest_bid", "last_sale"])
for i, key in enumerate(url_keys):
sid = f"stockx-{i % 5}" # rotate across five sticky sessions
api = f"https://stockx.com/api/products/{key}?includes=market¤cy=USD&country=US"
try:
body = fetch(api, session_id=sid)
if looks_blocked(body):
print("blocked:", key); continue
product = as_json(body)["Product"]
for child in product.get("children", {}).values():
cm = child.get("market", {})
w.writerow([key, child.get("shoeSize"),
cm.get("lowestAsk"), cm.get("highestBid"), cm.get("lastSale")])
except Exception as e:
print("error:", key, e)
time.sleep(2) # StockX rate-limits hard; stay polite
Five concurrent sessions with a two-second gap is a sane starting point. Push concurrency slowly and watch your block rate before you scale up.
Handle 403s, PerimeterX Challenges, and Rate Limits
Blocks on StockX are not all the same, and each has a different fix. Detect them first so you never mistake a block page for real data:
def looks_blocked(body):
b = body.lower()
return ("px-captcha" in b
or "perimeterx" in b
or "access to this page has been denied" in b
or "press & hold" in b)
Then match the symptom to the cure:
| Symptom | Cause | Fix |
|---|---|---|
| HTTP 403 with block HTML | PerimeterX flagged the request | `render_js` + `premium_proxy` + `stealth` |
| Press-and-hold captcha | PerimeterX raised a challenge | Rotate to a fresh `session_id`, slow down |
| HTTP 200 but empty `children` | Soft block or wrong locale | Add `includes=market`, check `country_code` |
| HTTP 429 | Rate limit | Fewer concurrent sessions, longer delays |
| SparkProxy 530 | Scrape failed (timeout or block) | Retry on a new `session_id` with backoff |
The sneaky one is the HTTP 200 with an empty children map. That is a soft block or a locale mismatch dressed up as a valid response, and a naive script records it as "no sizes available" instead of "I got filtered." Always assert that children is non-empty before you trust the row. For the broader playbook on staying unblocked, see how to avoid getting your proxy blocked.
Frequently asked questions
FAQ
Not for the general public. StockX offers a Seller API to approved partners for managing their own listings, but there is no sanctioned endpoint that returns live asks and bids for arbitrary products. The public market data comes from the same internal JSON the website's front end calls.
PerimeterX blocks your request before it reaches a product. A plain HTTP client fails the TLS and JavaScript fingerprint checks instantly, and datacenter IPs are pre-flagged. You need a real browser (render_js) on a residential IP (premium_proxy) with a stable session to get past it.
Read the children map on the product JSON. Each entry is one size variant with its own market block, so children[uuid].market.lowestAsk and market.highestBid give you the ask and bid for that exact size. The top-level market only reports the best figures across all sizes.
lowestAsk is the cheapest a seller will sell for right now, highestBid is the most a buyer currently offers, and lastSale is the price the most recent trade actually closed at. The gap between bid and ask is the spread, and it tells you how liquid that size is.
Yes. The /activity endpoint returns recent individual sales (amount, size, timestamp), and the /chart endpoint returns a price time series. Both are keyed by the product uuid, and you can pass a variant's child uuid to get history for one specific size.
StockX's Terms of Use prohibit automated access, so this sits in a legal gray zone. When you scrape StockX data, keep it to public prices, respect robots.txt, throttle your requests, and never collect personal or account information. Reading public market data for research is different from running a purchase bot, but if you build a commercial product on it, get legal advice first.
Get 20% off your first month
Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.
Save up to 15% more on quarterly, half-yearly and yearly plans
Related articles

Proxy Acceptable Use Policies: What Providers Ban and Why
Proxy acceptable use policy explained: the targets, ports and account behaviours providers restrict, how violations are detected, and how to stay unsuspended.

Monthly vs Annual Proxy Plans: When Committing Pays Off
Is an annual proxy plan worth it? Break-even months for 5% to 30% term discounts, the resizing and vendor risks that erase them, and what to ask first.

Scraping API Pricing: How Credit Multipliers Set Real Cost
Scraping API pricing explained: how JS rendering, premium proxies, domain surcharges and billed failures multiply credit costs, with a worked estimate.
