Myntra is India's largest fashion marketplace, and its listing pages are a good source of price data: brand, selling price, MRP, discount, rating and sizes for every product. Getting that into a CSV takes one short Python script, but there are three traps on the way: a request that never answers, a page parameter that is ignored, and a listing price that isn't the price of the size you want.

The block: a request that never answers

import requests url = "https://www.myntra.com/men-tshirts" chrome = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" requests.get(url, headers={"User-Agent": chrome}, timeout=20) 
Request Result
requests, default or Chrome user agent connection opens, no bytes, timeout after 20 s
curl (HTTP/2) stream was not closed cleanly: INTERNAL_ERROR
Through Scrape.do, no parameters 200 in about 3 s, full page

Myntra sits behind Akamai Bot Manager (the _abck and bm_sz cookies). There is no error page to detect: the script just hangs. I route every request through Scrape.do with no extra parameters: no rendering, no premium proxies, 1 credit per request. In my tests 6 of 6 plain requests came back with the full page.

The data: one JSON object per page

Myntra renders on the server and puts all the data into one inline script, window.__myx = {...}. Listings keep their products in searchData.results.products, product pages keep everything in pdpData. No CSS selectors needed: find the assignment and let json.JSONDecoder.raw_decode read exactly one object from there.

Sponsored products are separate: organic results are in products, ads in plaProducts (18 of them on the men's T-shirts page), and any ad mixed into the API results carries isPLA. Skip those.

Trap 2: ?p=2 returns page 1

Ask the HTML for page 2, 3 or 50 and you get the first 50 products again. On a listing sorted by newest, page 2 shared 49 of 50 products with page 1. The site pages through its search API, /gateway/v4/search/<slug>, which:

  1. answers 401 without the cookies a page visit sets (at, _abck, bm_sz and others), and
  2. ignores p and o as well unless the request carries a pagination-context header. Every response returns a new one in its headers; send it back to get the next 50.

So the loop is: visit the listing once, take its cookies (Scrape.do returns them in the scrape.do-cookies response header), then call the API page by page with those cookies and the cursor. Scrape.do forwards custom headers when you add extraHeaders=true and prefix them with sd-.

One more catch: roughly one visit in four came back with a single cookie and no at, and the API answered 401 to it. Repeat the visit until at= is there.

import csv import json import re import urllib.parse import requests TOKEN = "<your_token>" def fetch(url, headers=None): params = {"token": TOKEN, "url": url} if headers: params["extraHeaders"] = "true" # forward the sd- headers to Myntra  headers = {f"sd-{k}": v for k, v in headers.items()} response = requests.get("https://api.scrape.do/", params=params, headers=headers, timeout=120) if response.status_code != 200: raise RuntimeError(f"HTTP {response.status_code} for {url}") return response MYX_RE = re.compile(r"window\.__myx\s*=\s*") def extract_myx(html): match = MYX_RE.search(html) if not match: return {} data, _ = json.JSONDecoder().raw_decode(html, match.end()) return data def to_row(item): price, mrp = item.get("price"), item.get("mrp") return { "product_id": item.get("productId"), "brand": item.get("brand"), "name": item.get("productName"), "price_inr": price, "mrp_inr": mrp, "discount_pct": round((mrp - price) / mrp * 100) if price and mrp else 0, "rating": round(item.get("rating") or 0, 2), "rating_count": item.get("ratingCount"), "sizes": item.get("sizes"), "url": "https://www.myntra.com/" + item.get("landingPageUrl", ""), } LISTING_URL = "https://www.myntra.com/men-tshirts?sort=new" PAGES = 3 # 1. Visit the listing for its session cookies. Repeat until Myntra hands out "at". for _ in range(4): visit = fetch(LISTING_URL) cookies = visit.headers.get("scrape.do-cookies", "") if re.search(r"(^|;\s*)at=", cookies): break total = extract_myx(visit.text)["searchData"]["results"]["totalCount"] # 2. Page through the search API, sending back the cursor each response returns. parsed = urllib.parse.urlparse(LISTING_URL) rows, cursor = {}, "" for page in range(1, PAGES + 1): query = urllib.parse.parse_qsl(parsed.query) + [ ("rows", "50"), ("o", str(max(0, (page - 1) * 50 - 1))), ("plaEnabled", "false"), ("p", str(page)), ] api_url = f"https://www.myntra.com/gateway/v4/search{parsed.path}?{urllib.parse.urlencode(query)}" headers = {"Accept": "application/json", "x-meta-app": "channel=web", "app": "web", "X-myntraweb": "Yes", "Cookie": cookies} if cursor: headers["pagination-context"] = cursor response = fetch(api_url, headers) data = response.json() for item in data.get("products", []): if not item.get("isPLA"): # skip sponsored cards  rows.setdefault(item["productId"], to_row(item)) cursor = response.headers.get("pagination-context", "") if not data.get("hasNextPage") or not cursor: break rows = list(rows.values()) with open("myntra-listing.csv", "w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=list(rows[0].keys())) writer.writeheader() writer.writerows(rows) print(f"{total} products listed, saved {len(rows)} to myntra-listing.csv") 

Three API pages plus the visit: 4 credits, about 15 seconds, 150 distinct products. In my runs it came back with 150 every time.

Trap 3: the listing price is not the size price

A listing card has one price. On the product page every size lists its sellers, each with its own price and exact stock in pdpData.sizes[].sizeSellerData:

def size_rows(url): pdp = extract_myx(fetch(url).text)["pdpData"] sellers = {s["sellerPartnerId"]: s["sellerName"] for s in pdp.get("sellers", [])} rows = [] for size in pdp["sizes"]: for offer in size.get("sizeSellerData") or [{}]: rows.append({ "size": size["label"], "seller": sellers.get(offer.get("sellerPartnerId")), "price_inr": offer.get("discountedPrice"), "mrp_inr": offer.get("mrp"), "units": offer.get("availableCount", 0) if size["available"] else 0, }) return pdp["name"], rows name, rows = size_rows("https://www.myntra.com/12027436") print(name) for row in rows: print(row["size"], row["seller"], row["price_inr"], row["units"]) 
Levis Men Soft Pure Cotton Round Neck Half Sleeve Tshirt S Vision Star 402 398 M MODENIK LIFESTYLE PRIVATE LIMITED 389 83 M Vision Star 402 1261 L Vision Star 402 927 XL MODENIK LIFESTYLE PRIVATE LIMITED 389 303 

This T-shirt is listed at ₹389. That price only exists in M and XL, from one seller; in S and L the only seller charges ₹402. If you track prices for a specific size, read the product page.

Skip the boilerplate

The same logic is packaged as an open-source library, myntra-scraper on GitHub and PyPI, with session refresh on 401, retries and CSV output:

pip install myntra-scraper export SCRAPEDO_TOKEN=your_token myntra-scraper listing "https://www.myntra.com/men-tshirts?sort=new" --pages 3 -f csv -o myntra-listing.csv myntra-scraper product -i myntra-listing.csv --limit 10 --sizes -f csv -o myntra-sizes.csv 

There is also a no-code version on Apify (Myntra Scraper) and a browser demo on Hugging Face.

Run it at scale with Scrape.do

Every request here is a plain Scrape.do call: no browser, no proxy setup, 1 credit each. Five listing pages (250 products) cost 6 credits, so the free plan's 1,000 monthly credits go a long way. Get 1000 free credits and start scraping with Scrape.do.

The scripts read listing, search and product pages, which Myntra's robots.txt allows (Allow: /), and never touch cart, checkout or account paths. Product data is not personal data; read Myntra's terms for your use case and keep request rates modest.