How to Scrape Nike Product Data and SNKRS Drops
How to scrape Nike product data and SNKRS launch times from Nike's own feed: locale filters, the daylight-saving clock trap, SKU levels and a polling loop.
Most guides on how to scrape nike product data stop at the product page. That is the easy half. The half that pays is the launch calendar: which shoe drops, in which market, at which second, and whether the entry window closes 30 minutes later. Everything below was measured against Nike's own endpoints on 2026-10-09, including the one timestamp field that quietly points at 2027.
The drop calendar lives in a feed, not on a page
Nike's launch pages are rendered from a public product feed at api.nike.com/product_feed/threads/v2/. The SNKRS surface is one channel inside it, channelName: "SNKRS Web", with the channel id 010794e5-35fe-4e32-aaff-cd2c74f89d61. The same id appears in the page source of nike.com/gb/launch as well as the US launch page, so it is not market-specific. The market comes from filters instead.
Two filters decide how much you get back. Without upcoming(true) the US SNKRS channel reported totalResources: 6547, which is the channel's entire thread set, past releases included. With it, the whole upcoming US calendar arrived as 11 thread objects in one 678,365-byte response:
CH=010794e5-35fe-4e32-aaff-cd2c74f89d61
curl -s --compressed \
"https://api.nike.com/product_feed/threads/v2/?filter=marketplace(US)&filter=language(en)&filter=channelId($CH)&filter=upcoming(true)&anchor=0&count=20" \
| python -c "import sys,json; d=json.load(sys.stdin); print(d['pages']['totalResources'], 'threads')"
Eleven threads held 15 launch SKUs, because a thread is a release, not a product. The Air Jordan 1 High OG "Royal" release is four objects sharing one thread: men's at 185 USD, big kids' at 140, little kids' at 85, toddler at 70, each with its own style colour and its own inventory.
One trap before you build URLs. The launch page slug is not productContent.slug. It sits at publishedContent.properties.seo.slug. Using the product slug gave me https://www.nike.com/launch/t/air-jordan-1-high-og-royal-mens-shoes-p8vLeLAd and HTTP 404; the seo slug gave https://www.nike.com/launch/t/air-jordan-1-high-og-royal and HTTP 200.
Reading a launch record: LEO, DAN and the field that lies
Every releasing product carries a launchView. Products that are not launching do not, and their merchProduct.publishType reads FLOW instead of LAUNCH. Across 300 US threads I scanned, 120 product records were LAUNCH and 102 were FLOW, and merchProduct.hardLaunch was true for exactly the LAUNCH set.
| Field | Where it lives | What it means |
|---|---|---|
| `launchView.method` | feed and PDP | `LEO` first-come, `DAN` draw |
| `launchView.startEntryDate` | feed and PDP | the drop instant, UTC, millisecond precision |
| `launchView.stopEntryDate` | feed and PDP | draw close time; `null` for `LEO` |
| `launchView.launchState` | PDP only | e.g. `NOT_ACCEPTING_ENTRIES` |
| `launchView.stopNotifyDate` | PDP only | notify cutoff, 13:45Z for a 14:00Z drop |
| `merchProduct.quantityLimit` | feed | Nike's per-member cap, `1` on every launch I read |
| `merchProduct.commerceStartDate` | feed | not the drop time, see below |
| `availableSkus[].level` | feed | `HIGH`, `MEDIUM`, `LOW`, `OOS` per size |
commerceStartDate is the field that wastes a week. For style colour IQ5495-005, which actually drops at 2026-10-10T14:00:00.000Z, commerceStartDate read 2027-05-23T23:52:10.000Z and softLaunchDate read 2026-09-26T16:25:00.000Z. Three plausible timestamps, one correct answer. Read launchView.startEntryDate and ignore the other two.
The method split matters more than it looks. LEO releases carried stopEntryDate: null. The Japanese DAN releases carried startEntryDate: 2026-10-10T00:00:00.000Z and stopEntryDate: 2026-10-10T00:30:00.000Z, a fixed 30-minute entry window. A LEO drop is a race; a DAN drop is a half-hour form.
Inventory arrives as a coarse level, never a count. Across the same 300 threads, availableSkus[].level broke down as 614 HIGH, 92 MEDIUM, 1,808 LOW and 1,110 OOS. If your dashboard promises unit counts, Nike will not supply them.
import gzip
import json
import urllib.request
CHANNEL = "010794e5-35fe-4e32-aaff-cd2c74f89d61"
FEED = "https://api.nike.com/product_feed/threads/v2/"
def fetch(marketplace, language, count=40, upcoming=True):
q = [
"filter=marketplace(%s)" % marketplace,
"filter=language(%s)" % language,
"filter=channelId(%s)" % CHANNEL,
]
if upcoming:
q.append("filter=upcoming(true)")
q += ["anchor=0", "count=%d" % count]
req = urllib.request.Request(
FEED + "?" + "&".join(q),
headers={"User-Agent": "Mozilla/5.0", "Accept-Encoding": "gzip"},
)
with urllib.request.urlopen(req, timeout=60) as r:
raw = r.read()
if r.headers.get("Content-Encoding") == "gzip":
raw = gzip.decompress(raw)
return json.loads(raw)
def launches(doc):
for thread in doc["objects"]:
seo = thread["publishedContent"]["properties"].get("seo") or {}
for item in thread.get("productInfo") or []:
view = item.get("launchView")
if not view:
continue # publishType FLOW: no launch, no entry window
product = item["merchProduct"]
price = item["merchPrice"]
levels = [s["level"] for s in item.get("availableSkus") or []]
yield {
"style_color": product["styleColor"],
"method": view["method"],
"start": view["startEntryDate"],
"stop": view.get("stopEntryDate"),
"limit": product.get("quantityLimit"),
"price": price["currentPrice"],
"currency": price["currency"],
"sizes_live": sum(1 for lv in levels if lv != "OOS"),
"url": "https://www.nike.com/launch/t/%s" % seo.get("slug"),
}
for row in sorted(launches(fetch("US", "en")), key=lambda r: r["start"]):
print(row)
Scraping at scale? Skip the blocks.
Fast, unblockable datacentre proxies with unlimited bandwidth.
Why 14:00Z is wrong for four months of the year
Here is the finding I did not expect. Of the 120 US launch records I pulled, 99 had a startEntryDate time-of-day of exactly 14:00:00 UTC and 19 had exactly 15:00:00 UTC. Two were outliers: 2026-07-29T21:25:00Z and 2026-10-06T20:00:00Z.
The split is not random, and it is not two different drop slots. Every 15:00:00Z launch fell between 2026-01-31 and 2026-03-05. Every 14:00:00Z launch fell between 2026-03-12 and 2026-10-17. United States daylight saving time began on 2026-03-08, right in the gap.
So the SNKRS US drop slot is not a UTC constant at all. It is 10:00 in America/New_York, stored in UTC, and the stored value moves by one hour twice a year. Hard-code 14:00Z in a scheduler and every drop after 2026-11-01 fires 60 minutes early. Convert through the zone instead:
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
DROP_TZ = ZoneInfo("America/New_York")
def local_drop(start_entry_date):
naive = datetime.strptime(start_entry_date, "%Y-%m-%dT%H:%M:%S.%fZ")
return naive.replace(tzinfo=timezone.utc).astimezone(DROP_TZ)
for stamp in ("2026-03-05T15:00:00.000Z", "2026-10-10T14:00:00.000Z"):
print(stamp, "->", local_drop(stamp).strftime("%Y-%m-%d %H:%M %Z"))
Both print 10:00, one as EST and one as EDT. On Windows that block raises ZoneInfoNotFoundError until you pip install tzdata, because zoneinfo has no system tz database to read; on Linux and macOS it works as written. The two outliers are the reason you still read the field per release rather than deriving it: Nike does schedule off-slot drops, and a derived calendar would have missed both.
Sync your clock to Nike, not to your own box
A scraper that wants the first post-drop snapshot has to know what time it is at the origin, not what time its own kernel thinks it is. On this machine the gap was real: across three calls, the local clock read 1.53, 1.65 and 2.68 seconds behind the Date header Nike's edge returned. That is enough to miss the transition on a LEO release where popular sizes go from HIGH to OOS inside a minute.
The Date header is second-granular, so it will not give you millisecond truth, but it bounds your error and it costs one request:
import email.utils
import time
import urllib.request
from datetime import datetime, timezone
def server_skew(url, samples=3):
"""Median seconds our clock runs ahead of the origin. Negative means behind."""
deltas = []
for _ in range(samples):
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
t0 = time.time()
with urllib.request.urlopen(req, timeout=30) as r:
t1 = time.time()
served = email.utils.parsedate_to_datetime(r.headers["Date"])
deltas.append((t0 + t1) / 2 - served.timestamp())
return sorted(deltas)[len(deltas) // 2]
def sleep_until(start_entry_date, skew, lead=1.5):
target = datetime.strptime(start_entry_date, "%Y-%m-%dT%H:%M:%S.%fZ")
target = target.replace(tzinfo=timezone.utc).timestamp()
wait = (target + skew) - time.time() - lead
if wait > 0:
time.sleep(wait)
Cadence is set by the edge, not by your appetite. The feed returns Cache-Control: private, no-transform, max-age=30, and four polls spread over roughly 18 seconds came back byte-identical (SHA-256 prefix 72a33aee). Polling the feed every second buys nothing but a block risk. Poll the feed on a 30-second floor to learn what is dropping, then switch to the launch page, where launchState and availableSkus move, for the during window. Our notes on picking a proxy rotation interval cover the same arithmetic from the IP side.
The same shoe, four markets, four answers
Pulling the upcoming calendar for four marketplaces on the same afternoon produced this. Every value is Nike's own, read on 2026-10-09:
| Style colour | United States | Great Britain | Japan | Germany |
|---|---|---|---|---|
| IQ5495-005 (AJ1 High OG "Royal") | 2026-10-10 14:00Z, LEO, 185 USD | not listed | 2026-10-10 00:00Z, **DAN**, 26,730 JPY | not listed |
| IV4517-001 (Air Max Goadome Low) | 2026-10-15 14:00Z, LEO, 175 USD | 2026-10-10 07:00Z, LEO, 169.99 GBP | not listed | 2026-10-10 07:00Z, LEO, 184.99 EUR |
| IM2523-600 (Blazy "Varsity Red") | 2026-10-15 14:00Z, LEO, 95 USD | not listed | 2026-10-16 00:00Z, LEO, 13,200 JPY | not listed |
| HQ7978-001 (Jordan 5 Retro "Halloween") | 2026-10-17 14:00Z, LEO, 215 USD | not listed | 2026-10-17 00:00Z, LEO, 32,340 JPY | not listed |
| Upcoming threads in channel | 11 | 3 | 13 | 3 |
Three things fall out of that grid. First, the release mechanism changes by market for the same style colour: the Royal is a first-come LEO race in the US and a 30-minute DAN draw in Japan. A monitor that stores one method per style colour gets Japan wrong, and it will not know it, because both records look well formed.
Second, the stagger is days, not hours. IV4517-001 lands in Britain and Germany on 2026-10-10 and in the US on 2026-10-15, five days later. Both European entries shared the thread slug air-max-goadome-low-black-emea and the same 07:00:00Z instant, which is one EMEA launch window serving two currencies.
Third, the calendars are different sizes. Japan had 13 upcoming threads to the US 11, while Britain and Germany had 3 each. A "global SNKRS calendar" scraped from one market is a US calendar with a confident name on it. If you care about what happens to these shoes after the drop, the resale side is a separate target with separate defences, covered in our guide to scraping StockX prices, bids and asks. A companion guide to the same problem on Adidas follows this one on the blog.
Locale pairs that return an empty calendar
This is the failure that looks like success. The feed's marketplace and language filters are validated as a pair, and a wrong pair returns HTTP 200 with nothing in it:
| marketplace | language | Result |
|---|---|---|
| US | en | 200, `totalResources: 6547` |
| GB | en | **200, `totalResources: 0`** |
| GB | en-GB | 200, `totalResources: 2609` |
| JP | ja | 200, `totalResources: 4112` |
| DE | de | 200, `totalResources: 2564` |
| FR | fr | 200, `totalResources: 2582` |
| IT | it | 200, `totalResources: 2557` |
| NL | nl | 200, `totalResources: 2622` |
| KR | ko | 200, `totalResources: 1259` |
| CN | zh-Hans | 200, `totalResources: 1881` |
| UK | en | 200, `totalResources: 0` |
| CA | en-CA | **400, `INVALID_LANGUAGE`** |
The United States is the only market in that set that accepts a bare en. Britain needs en-GB (lower-case en-gb also worked). Ask for CA with en-CA, which is the obvious guess, and the API refuses with a readable body:
{"message":"Validation Failed","errors":[{"code":"INVALID_LANGUAGE","field":"/language","message":"errors=[en-CA]"}]}
A 400 you will notice. The 200-with-zero case you will not, so assert on it. Treat an empty upcoming calendar as a configuration bug until a second market proves otherwise:
import json
import urllib.error
def calendar_size(marketplace, language, fetcher):
try:
doc = fetcher(marketplace, language, count=1)
except urllib.error.HTTPError as exc:
payload = json.loads(exc.read().decode("utf-8", "replace"))
return "HTTP %d %s" % (exc.code, payload["errors"][0]["code"])
size = doc["pages"]["totalResources"]
if size == 0:
raise RuntimeError(
"%s/%s returned 200 with an empty calendar: check the language token"
% (marketplace, language))
return size
Retailers that key their catalogue off a locale pair behave this way often. The store-code equivalent on another site is laid out in how to scrape ASOS product data, and the lesson transfers: validate the pair before you trust the count.
Catalogue data: the easy half, in one parse
Routine product data needs no API reverse-engineering. A Nike product page at /t/ ships exactly one JSON-LD block, tagged data-lu-target="product-schema", and it is a ProductGroup with variesBy: ["https://schema.org/color", "https://schema.org/size"]. On the Royal men's page it held 21 variants, 18 of which carried an Offer.
Now the part that decides your architecture. Those 18 offers carried price, priceCurrency, itemCondition and url. Not one of them carried availability. Zero out of 18. Nike's HTML tells you what a size costs and never whether you can buy it.
The server-rendered React payload does not rescue you either. In __NEXT_DATA__, props.pageProps.selectedProduct.price was null and its skus array was empty, next to a field literally named isSkusRequestInFlight. Sizes and stock are fetched client-side after paint. What __NEXT_DATA__ does carry, and the feed does not, is the live launchView.launchState (NOT_ACCEPTING_ENTRIES) and stopNotifyDate (2026-10-10T13:45:00.000Z, exactly 15 minutes before the drop).
That gives a clean division of labour: price and GTINs from the page's JSON-LD, stock levels and schedule from the feed, live launch state from the page's React payload.
import json
import re
import urllib.request
PDP = ("https://www.nike.com/t/"
"air-jordan-1-high-og-royal-mens-shoes-p8vLeLAd/IQ5495-005")
req = urllib.request.Request(PDP, headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(req, timeout=40) as r:
html = r.read().decode("utf-8", "replace")
block = re.search(r'application/ld\+json[^>]*>(.*?)</script>', html, re.S).group(1)
group = json.loads(block)[0]
print(group["@type"], group["variesBy"])
for variant in group["hasVariant"]:
offer = variant.get("offers")
if not offer:
continue # 3 of 21 variants carried no Offer at all
print(variant["size"], variant["gtin"], offer["price"], offer["priceCurrency"],
"availability:", offer.get("availability", "ABSENT"))
state = json.loads(
re.search(r'id="__NEXT_DATA__"[^>]*>(.*?)</script>', html, re.S).group(1))
view = state["props"]["pageProps"].get("launchView") or {}
print(view.get("launchState"), view.get("startEntryDate"), view.get("stopNotifyDate"))
What robots.txt allows and the Terms of Use forbid
Read both documents, because they do not agree, and pretending otherwise is how teams get surprised.
https://www.nike.com/robots.txt opens with a comment that is hard to misread: # www.nike.com robots.txt -- just crawl it. For User-agent: it disallows a short list, including /member/inbox, /member/settings, /p/, */checkout/, /pdf/ and /fragments/recommendations-carousel. Several Chinese search crawlers are then restricted to /cn plus static assets. There is no Crawl-delay.
It also advertises seven sitemaps, and two of them are the map of this whole article:
| Sitemap index | Locale files | What it enumerates |
|---|---|---|
| `sitemap-v2-snkrsweb-index.xml` | 59 | `/launch/t/ |
| `sitemap-v2-pdp-index.xml` | 62 | `/t/ |
Comparing the two lists is informative. Four locales have a product sitemap and no SNKRS one (en-ca, fr-ca, en-jp, zh-hans-cn), and one has a SNKRS sitemap and no product one (no-no), which nets out to the three-file difference. The en-us SNKRS file alone listed 6,527 launch URLs across 7.5 MB, each entry carrying changefreq: daily, priority: 0.5, inline hreflang alternates for every sibling locale, and no lastmod. No lastmod means the sitemap cannot tell you what changed, which is precisely why the launch calendar has to come from the feed.
Against that, Nike's Terms of Use (the copy served from agreementservice.svs.nike.com, last updated October 2025) prohibits, under "User Code of Conduct", that you "Develop or use any application or automated technology, other than as approved by NIKE, to interact with the Services" and "Use any data mining, robots, scraping, or similar data gathering methods to access, use, scrape, or extract content or data from the Services". It separately forbids ignoring robots.txt or bypassing access-limiting measures, forbids using the data to train machine learning models, and forbids reselling Nike data without consent.
So: a permissive crawler file, a restrictive contract. This is not legal advice, and the resolution is not technical. It is a decision your organisation makes with its own counsel about contract risk, after reading both documents rather than one.
Where this guide stops
This article covers reading public pages and the public feed that renders them. It does not help you buy anything, and that boundary is deliberate.
Nothing here touches account creation, login, /checkout/, SNKRS entry submission, draw entries, or any form of queue-jumping. We will not help with purchase automation, and there is no section coming later that does. If you are after a cook-group bot, this is the wrong document. Three concrete commitments follow from that, and they also keep your crawler boring:
- Public, unauthenticated surfaces only. Nike's robots file disallows
*/checkout/and the member pages; so do we. - Respect the published cadence. The feed declares
max-age=30; a 30-second floor is the stated budget, and we stay on it. - Honour
quantityLimit. Nike sets it to1on launches. A per-member cap of one is a statement of intent, not an obstacle to route around with more accounts.
Price history, size-availability curves, regional launch timing and resale spread are all legitimate research questions you can answer inside those limits. Our guide to ethical scraping and rate limiting sets out the same discipline as a reusable checklist. Infrastructure sizing for genuinely spiky drop-time reads, as opposed to automation, is covered in datacenter proxies for sneaker and retail automation.
The defences you will actually meet
Nike sits behind Akamai: responses carry Server: unified-edge-router or AkamaiGHost, an Akamai-GRN identifier, and Server-Timing: ak_p. What that edge actually objects to is narrower and stranger than the usual advice suggests. Same URL, same IP, four User-Agent strings, measured back to back:
| Request | api.nike.com feed | www.nike.com product page |
|---|---|---|
| Chrome UA string | 200 | 200 |
| default `curl/8.x` UA | 200 | 200 |
| no `User-Agent` header at all | 200 | 200 |
| `python-requests/2.32.3` | **403** | 200 |
The absence of a browser UA is fine. The string python-requests/2.32.3 is not. One library default accounts for the entire difference, and the 403 it earns is a classic Akamai edge deny: 411 bytes containing , a Reference # and an errors.edgesuite.net link. Set any other UA and this layer stops existing. Note the asymmetry, because it tells you where the rule is enforced: the same string sailed through on the product page and only the API edge rejected it. If you later need to match a real browser's TLS handshake rather than just its UA, web scraping with curl_cffi covers the impersonation side, and curl_cffi is actively maintained: PyPI shows 0.16.3 published 2026-09-02, and the GitHub repository's most recent commit as I write is 04dc8c15, dated 2026-10-09.
The second defence is geography, and it is not a block. Requesting https://www.nike.com/ from this host produced two redirects ending at https://www.nike.in/ with HTTP 403. A request to www.nike.com/launch from the same host came back with Set-Cookie: geoloc=cc=IN,rc=KA,tp=vhigh,tz=GMT+5.50. Nike derives country, region and timezone from the exit IP and routes the HTML storefront to a regional site before any content decision happens.
The split matters for how you budget proxies. The feed ignored geography: marketplace(GB) with language(en-GB) returned 2,609 threads to the same Indian IP that the HTML root refused. So the launch calendar needs no country-pinned exit at all, and only the page-level work does: the product page, the launch page, and anything you want to see as a shopper in that market sees it. Deeper Akamai behaviour is in bypassing Akamai Bot Manager.
Four states, and only one of them is a real failure, so classify on content rather than status:
def classify(status, body):
if status == 403 and b"Access Denied" in body:
return "edge-deny" # Server: AkamaiGHost, ~411 bytes
if status == 400 and b"INVALID_LANGUAGE" in body:
return "bad-locale"
if status == 200 and b'"totalResources":0' in body.replace(b" ", b""):
return "empty-calendar" # valid tokens, wrong pair
if status == 200 and b'"objects"' in body:
return "ok"
return "unknown-%d" % status
Four rungs from empty to complete
Start cheap. Nike's feed answers an unauthenticated request from a datacenter IP, so most of this job needs no proxy at all.
Rung one, plain fetch. A sane User-Agent and gzip against api.nike.com. This is what produced every number above. Through the SparkProxy Scraping API it is render_js=false, which is a plain fetch at 1 credit rather than the default rendered path at 5. New accounts get 1,000 free credits, and the arithmetic is broken down in scraping API credit pricing explained.
Rung two, pin the exit country for page work only. The feed does not need it, as the GB result above shows. The HTML surfaces do, because that is where the geo redirect lives, so add country_code (+5 credits) when you want the launch page or product page as a shopper in that market sees it. SparkProxy's pool covers 80+ countries over 1M+ premium IPs with unlimited bandwidth, so polling on a 30-second floor costs you plan concurrency rather than traffic.
Rung three, batch the catalogue sweep. Product pages across many style colours are a batch job: batch mode requires render_js=false, which is all a JSON-LD parse needs, and the whole batch costs 1 credit regardless of URL count.
Rung four, render only the launch page. Rendering buys you one thing here: the live launchState in __NEXT_DATA__ during the drop window. That is render_js=true at 5 credits. Do not stack extract_rules with json_response, because extract_rules replaces the response with an extracted object instead of wrapping it.
import base64
import json
import urllib.parse
import urllib.request
API = "https://scrape.sparkproxy.io/api/v1"
API_KEY = "YOUR_SPARKPROXY_KEY"
def through_sparkproxy(target, country=None, render=False):
params = {
"url": target,
"render_js": "true" if render else "false",
"proxy_type": "premium",
"json_response": "true", # the only source of the envelope
}
if country:
params["country_code"] = country
req = urllib.request.Request(
API + "?" + urllib.parse.urlencode(params),
headers={"X-API-Key": API_KEY},
)
with urllib.request.urlopen(req, timeout=180) as r:
envelope = json.load(r)
print("target %s in %s ms, %s credits" % (
envelope.get("status_code"), envelope.get("duration_ms"),
envelope.get("credits_used")))
# The envelope body is base64. Decode before matching anything.
return base64.b64decode(envelope["body"]).decode("utf-8", "replace")
GB_FEED = ("https://api.nike.com/product_feed/threads/v2/"
"?filter=marketplace(GB)&filter=language(en-GB)"
"&filter=channelId(010794e5-35fe-4e32-aaff-cd2c74f89d61)"
"&filter=upcoming(true)&anchor=0&count=20")
# Rung one: no country needed, render off. 1 credit.
calendar = json.loads(through_sparkproxy(GB_FEED))
print(calendar["pages"]["totalResources"], "upcoming GB threads")
# Rung two: the launch page as a GB shopper sees it. 1 + 5 credits.
for thread in calendar["objects"]:
slug = thread["publishedContent"]["properties"]["seo"]["slug"]
page = through_sparkproxy("https://www.nike.com/gb/launch/t/" + slug,
country="GB")
print(slug, len(page), "bytes")
Rendering, where you need it, is Playwright's job; it is current too, with 1.63.0 on PyPI dated 2026-09-15 and commits on microsoft/playwright-python from 2026-10-08. For the parts of the job that are a rendered page rather than a feed, session-aware proxy rotation for JavaScript-rendered sites is the pattern to copy. SparkProxy plans start from $75/month with unlimited bandwidth, and there is a 24-hour free trial with no card if you want to re-run the GB and JP calendar checks above for yourself before committing.
Build it in this order: calendar first, clock second, catalogue last. The catalogue will still be there tomorrow. The 11 upcoming threads will not.
Frequently asked questions
FAQ
Nike publishes no documented developer API for retail product data. The storefront is rendered from api.nike.com/product_feed/threads/v2/, which answers unauthenticated requests, but it is an internal endpoint, it is not documented for third parties, and Nike's Terms of Use restrict automated access regardless of whether the endpoint responds.
Query the SNKRS Web channel of the product feed with filter=upcoming(true) and read launchView.startEntryDate for each product. Ignore commerceStartDate and softLaunchDate; on one 2026-10-10 release they read 2027-05-23 and 2026-09-26 respectively.
Almost always a mismatched marketplace and language pair. marketplace(GB)&language(en) returns HTTP 200 with totalResources: 0, while marketplace(GB)&language(en-GB) returned 2,609. Only the US accepts a bare en, and a genuinely invalid token gives HTTP 400 with INVALID_LANGUAGE instead.
Only as a coarse level, not a unit count. The feed's availableSkus[].level reports HIGH, MEDIUM, LOW or OOS per SKU. The product page's JSON-LD does not help: of 18 size offers on one page, none carried an availability field at all.
Not for the feed. It served the full GB launch calendar to an Indian datacenter IP, so geography is not enforced there. The HTML surfaces are different: Nike redirects the root domain by exit IP and writes the detected country into a geoloc cookie, so a country-pinned exit is what you need for product and launch pages in a market other than your own.
Scraping public pages and contract compliance are different questions, and this is not legal advice. Nike's robots.txt is permissive about crawling while its Terms of Use, last updated October 2025, prohibit data mining, scraping and unapproved automated access. Read both, then take the question to your own counsel.
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
How to Bypass Fastly Next-Gen WAF
Bypass Fastly Next-Gen WAF (Signal Sciences) the right way: how its agent decides per signal at the edge, the 406 block, and why clean IPs beat header tricks.
How to Bypass Vercel BotID When Web Scraping
Bypass Vercel BotID: confirm it in one request, read the Kasada engine underneath, see what a block really returns, and know where the ceiling sits.
Anubis Proof of Work Bypass: What Solving Really Costs
Anubis proof of work bypass read from the MIT-licensed source: the SHA-256 loop, bits versus nibbles difficulty, measured solve costs, and when to stop.
