# How to Scrape IKEA: Search, Categories, Prices, and Stock with Python > Source: https://scrape.do/blog/ikea-scraping/ Published: 2026-06-03 · Updated: 2026-07-22 · Authors: Serhat Kurtulus · Categories: Scraping Use Cases Fetching an IKEA product page hands back over a megabyte of HTML with no catalog data we can point a selector at, and the JSON-LD inside describes breadcrumbs and a 3D model instead of the product. Meanwhile prices, discounts, ratings, and live stock status for the whole catalog sit a layer deeper, fully structured, waiting for anyone who finds where they actually live. We will pull all of it into three CSVs (search results, complete categories, and per-product price and stock) using nothing but `requests`. [Full working code on GitHub ⚙](https://github.com/scrape-do/scrapedo-scrapers) ## Where IKEA Keeps Its Product Data IKEA.com is a React storefront, and the HTML it serves is scaffolding. Watching the Network tab with the Fetch/XHR filter on while browsing reveals two real data sources, and neither is the page markup. The first is an internal search and listing API on its own subdomain: `sik.search.blue.cdtapps.com`. Search results load through `/us/en/search-result-page`, category listings through `/us/en/product-list-page`, both returning clean JSON. Store and language ride in the URL path, so `/us/en/` pins the US store without any cookie or geolocation dance. The second source is the product pages themselves, which [embed their entire data payload](https://scrape.do/blog/youtube-scraping/) in `', response.text, re.S) product, availability, page_product = {}, {}, {} for b in blocks: try: d = json.loads(b) except json.JSONDecodeError: continue if "product" in d and "availabilityResponse" in d: product = d["product"] availability = (d["availabilityResponse"] or {}).get("availability", {}) elif "pageProps" in d: page_product = d["pageProps"].get("product", {}) if not product: # dead URLs return a 200 page shell with no hydrate product block print(f"{url}: no product data found, skipping") continue ``` The guard is the 200-shell defense: a URL that fetches fine but carries no product block gets reported and skipped instead of producing a row of empty cells. ### Checking Stock and Delivery The availability object is the stock checker. `homeDelivery.stockStatus` gives a level (`HIGH_IN_STOCK` on every product we tested, with low and out-of-stock variants in the same enum), and the `homeDelivery.isAvailable` and `clickCollect.isAvailable` booleans split online from in-store pickup. Run this on a schedule, alert when a status transitions, and the restock-refresh ritual becomes a cron job. ```python measures = (product.get("packageMeasurements") or [{}])[0] rows.append({ "item_no": product.get("visibleItemNo"), "name": product.get("name"), "type": product.get("typeName"), "price": product.get("price"), "currency": product.get("currencyCode"), "review_count": page_product.get("reviewCount", ""), "stock_status": (availability.get("homeDelivery") or {}).get("stockStatus", ""), "home_delivery": (availability.get("homeDelivery") or {}).get("isAvailable", ""), "click_collect": (availability.get("clickCollect") or {}).get("isAvailable", ""), "packages": product.get("numberOfPackages"), "weight": (measures.get("weight") or {}).get("text", ""), "url": url, }) print(f"{product.get('name')}: ${product.get('price')} | {rows[-1]['stock_status']}") ``` `visibleItemNo` is the dotted article number printed on IKEA labels (205.220.46), handy for matching rows against receipts or store apps. Weight arrives pre-formatted ("88 lb 1 oz") next to numeric values; we export the readable form. ### Export to CSV ```python # 3. Export with open("product-details.csv", "w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=rows[0].keys()) writer.writeheader() writer.writerows(rows) print(f"total: {len(rows)} products") ``` The run covers a plain bookcase, a multi-package desk combination, and a KALLAX: ``` BILLY: $79 | HIGH_IN_STOCK LAGKAPTEN / ALEX: $239.99 | HIGH_IN_STOCK KALLAX: $79.99 | HIGH_IN_STOCK total: 3 products ``` ``` item_no,name,type,price,currency,review_count,stock_status,home_delivery,click_collect,packages,weight,url 205.220.46,BILLY,bookcase,79,USD,3088,HIGH_IN_STOCK,True,True,1,88 lb 1 oz,... 994.319.82,LAGKAPTEN / ALEX,desk,239.99,USD,755,HIGH_IN_STOCK,True,True,3,,... 802.758.87,KALLAX,shelf unit,79.99,USD,8928,HIGH_IN_STOCK,True,True,1,45 lb 8 oz,... ``` We also fed it a fabricated URL during validation; the shell guard caught it and the CSV stayed clean. ![BILLY product page showing price, rating count, and availability](/uploads/blog/ikea-scraping-billy-product-page.png) ![product-details.csv with stock status and delivery columns](/uploads/blog/ikea-scraping-csv-output-product.png) Price, discounts, reviews, and live stock per article number. The tracker has everything it needs. ## FAQ ### Does IKEA have a public API? No official public product API exists. The [community client libraries on GitHub](https://github.com/vrslev/ikea-api-client) wrap internal endpoints and break whenever those shift, and several are archived. The practical sources are the internal search API at `sik.search.blue.cdtapps.com` and the hydrate JSON on product pages: no authentication, plain JSON, and the same data the site itself renders. ### Can IKEA prices be tracked automatically? Yes. Every search and category row carries `salesPrice.numeral` (current price) and, only when an item is discounted, `salesPrice.previous.numeral`. Scheduled category runs cost two requests per category regardless of size, which makes catalog-wide daily snapshots cheap; diffing consecutive snapshots surfaces price drops and new discounts. ### How to check IKEA stock programmatically? Product pages embed an availability response with `stockStatus`, home delivery availability, and click-and-collect availability. Polling a URL list and alerting on status transitions covers the online-stock use case. Per-store shelf stock for a specific location is a different, authenticated surface and outside this guide's scope. ### Why do dead IKEA product URLs return 200? The React storefront serves its page shell for unknown routes instead of a 404 status, so HTTP codes cannot distinguish a live product from a dead link. The reliable check is content-based: a real product page contains a `text/hydrate` block with a `product` key, and a shell does not. --- Three surfaces, three CSVs, and not one CSS selector: the work on IKEA was locating the real sources, an internal API on a subdomain nobody links to and JSON blocks buried in the page. Each quirk we hit (a cursor that oversells, a listing with no pages, dead URLs wearing a 200) cost one guard clause, and the guards are all in the scripts above. [Get 1000 free credits and start scraping with Scrape.do](https://dashboard.scrape.do/signup)