How to scrape StockX
StockX is protected by PerimeterX (HUMAN). Plain requests tend to get a challenge page instead of products. StealthASF Ultra mode returns the real page: in our production test on 8 October 2026, the StockX sneakers page came back with HTTP 200 and its real title. This guide shows how to collect product pages from a browse page and read their fields.
1. Use Ultra mode
Set engine to ultra. It is our strongest mode, built for the hardest protected sites, and the mode that passed StockX in our tests. Ultra renders the page and supports extraction but does not run browser steps such as clicks. Every StockX product has its own URL, so you can reach the data you need by requesting pages directly.
2. Request a browse page with curl
Verify your email, create an API key in the dashboard and set STEALTHASF_API_KEY. The request fetches the sneakers browse page with links extraction. The data field then lists every link on the page with its text, including the product tiles.
curl --max-time 600 "https://stealthasf.com/v1/scrape" \
-H "x-api-key: $STEALTHASF_API_KEY" \
-H "content-type: application/json" \
--data-raw '{"url":"https://stockx.com/sneakers","engine":"ultra","extract":"links"}'Use any browse, brand or search URL from your browser in place of the example. Keep the 600-second client timeout; a protected page rendered in Ultra mode takes longer than a plain fetch.
3. What comes back
The JSON response includes the target's status, the rendered html, the engine and the credits_charged. With links extraction, data is a table of text and url. StockX product pages sit directly under the domain, such as a path made of the product name, so the product rows are the ones whose text reads like a product name and price. Navigation, help and account links are easy to drop by their path. If the page text is a challenge instead of products, the request was blocked: detected blocks return HTTP 422 and cost nothing.
4. Collect products in Python
This example uses the Python standard library only and stops on an API error, so an error never becomes a product row.
import json
import os
from urllib.error import HTTPError
from urllib.request import Request, urlopen
payload = {
"url": "https://stockx.com/sneakers",
"engine": "ultra",
"extract": "links"
}
request = Request(
"https://stealthasf.com/v1/scrape",
data=json.dumps(payload).encode("utf-8"),
headers={
"x-api-key": os.environ["STEALTHASF_API_KEY"],
"content-type": "application/json",
},
method="POST",
)
try:
with urlopen(request, timeout=600) as response:
result = json.load(response)
except HTTPError as error:
detail = error.read().decode("utf-8")
raise SystemExit(f"API error {error.code}: {detail}")
print("Target status:", result["status"])
print("Credits:", result["credits_charged"])
print(result.get("data"))The follow-up below keeps StockX links and reads JSON-LD from a page's HTML. Skip the navigation links before you send detail requests. Product pages that embed schema.org Product data give you the name, brand and offer details as JSON without CSS selectors.
import re
# result is the listing-page response from the example above.
detail_urls = sorted({url for _text, url in result["data"]["rows"] if "stockx.com/" in url})
print(len(detail_urls), "detail pages")
def json_ld(html):
"""Every JSON-LD block on the page that parses as JSON."""
blocks = re.findall(r"<script[^>]*application/ld\+json[^>]*>(.*?)</script>", html, re.S | re.I)
found = []
for block in blocks:
try:
found.append(json.loads(block))
except json.JSONDecodeError:
pass
return found
# Send each detail URL with the same payload, then read its fields:
# for item in json_ld(detail["html"]): print(item.get("@type"), item.get("name"))5. Fields and cost
Typical StockX fields are product name, style code, colorway, retail price, release date and the current market prices shown on the page. Use the product URL or style code as the key for each record, so repeated runs update the same row. Prices change often, so store a timestamp with every price you collect.
An Ultra request costs 50 credits including the first 1 MB of transfer, plus 10 credits per additional MB, rounded up to a whole credit. A solved CAPTCHA adds 25 credits. A browse page and 40 product pages at the base rate cost 2,050 credits. On Pro, 250,000 credits cover 5,000 Ultra requests at the base rate, and Scale covers 20,000. The exact figure for each page is in credits_charged. Blocked requests are never charged.
6. Track prices over time
Build your product list once, then request only those product pages on each run. Record the URL, engine, target status, job ID, credits and time with every price. Increase concurrency gradually within your plan limit, follow Retry-After on a 429 response and cap retries. If one URL keeps returning 422, send support the job ID. See the PerimeterX guide for more on this protection and the API reference for every field.