How to Scrape Flipkart Product Data: Prices & Specs
Scrape Flipkart product data: pull prices, specs, ratings, and offers, decode pid and lid, clear the bot wall, and pin pincode pricing with a scraping API.

To scrape Flipkart product data reliably, you fight on two fronts: pulling clean fields out of a page whose CSS class names are minified and rotate, and getting past Flipkart's bot wall and India-only geo gate before that page will even load. Most guides hand you a selector that works for a week, then goes quiet when the class hash changes or your datacenter IP gets flagged. This guide covers the whole pipeline for public product and price data: which fields to pull, how Flipkart's pid and lid identifiers actually work, how to catch a soft block that returns HTTP 200, why prices and offers move by pincode and login, and how to paginate search at volume. Every code sample uses SparkProxy's Scraping API, so the anti-bot and India-IP work is one request parameter instead of an infrastructure project you babysit.
Is scraping Flipkart product data legal?
Public product data is not a free-for-all, so get the framing right before you write a line of code. Two separate questions decide whether a project is defensible: was the access authorized, and is the data itself protected.
In the United States, the Ninth Circuit's ruling in hiQ Labs v. LinkedIn (2022) held that scraping data that is publicly accessible, meaning no login and no authentication wall, generally does not violate the Computer Fraud and Abuse Act. That is a US ruling about access, not a global license. Flipkart is an Indian marketplace, so Indian law is the relevant frame in practice. Flipkart's Terms of Use explicitly prohibit automated data collection, which makes aggressive scraping a potential breach of contract regardless of the CFAA. India's Digital Personal Data Protection Act, 2023 governs personal data, so anything that identifies a buyer or reviewer is a hard line you do not cross.
Guardrails that keep a Flipkart project on defensible ground:
- Collect public product data only: title, price, specs, rating, offers. Nothing behind a login, and no personal data about shoppers or reviewers.
- Rate-limit yourself and back off on errors so you are not degrading the service for real shoppers.
- Do not republish copyrighted assets (product images, full review text) beyond what fair dealing allows.
- If the data feeds a commercial product, run it past a lawyer. This is engineering guidance, not legal advice.
Price monitoring, catalog research, and competitive intelligence across Indian retail are common, legitimate uses of public data. For the business framing around that, see How E-commerce Companies Use Proxies for Competitive Intelligence.
What product data you can extract (fields reference)
A public Flipkart product page exposes a consistent set of fields. The exact DOM location drifts because Flipkart minifies its class names, but the fields themselves are stable. Here is the reference set worth pulling:
| Field | Where it lives | How to get it | Notes |
|---|---|---|---|
| Title | Product heading near the top | Heading `span`, or `og:title` meta | The most stable value is the `og:title` meta tag |
| Selling price | Buy box, large bold number | Nearest `₹` amount to the buy box | Flipkart calls it "Special Price" |
| MRP | Struck-through price above selling price | Strikethrough element near price | The list price before discount |
| Discount | Percent next to price | Text ending in "% off" | Derived from MRP and selling price |
| Rating | Rating pill next to title | Small badge with a value like `4.4` | Out of 5 |
| Ratings and reviews | Under the title | Text like "1,20,431 Ratings & 8,210 Reviews" | Indian digit grouping (lakh format) |
| Highlights | "Highlights" bullet list | `
| RAM, storage, key specs |
| Specifications | Spec tables lower on the page | Label and value cells in tables | The richest structured section |
| Seller | "Sold by" line | Seller name near the buy box | Tied to the `lid`, not the `pid` |
| Offers | "Available offers" list | ` | Bank, EMI, exchange, coupon offers |
| Delivery / availability | Buy box, below price | Delivery text after you set a pincode | Changes by pincode |
| pid | URL query string | `pid=` parameter | The product identifier, your primary key |
| lid | URL query string | `lid=` parameter | The listing (seller offer) identifier |
The pid is the anchor for everything. Store it as your primary key and every other field hangs off it. The next section explains why pid and lid are two different things, and why that distinction matters more on Flipkart than on any US marketplace.
Scraping at scale? Skip the blocks.
Fast, unblockable datacentre proxies with unlimited bandwidth.
Decode Flipkart URLs: pid, lid, and the item path
Flipkart product URLs look opaque, but they carry three identifiers, and knowing which is which saves you from silently mixing up products and sellers. A typical URL:
https://www.flipkart.com/samsung-galaxy-m35-5g/p/itm9d1f7a...?pid=MOBGWFHZ8ZAABCDE&lid=LSTMOBGWFHZ8ZAABCDE7XZKQF&marketplace=FLIPKART
Break it down:
| URL part | Example | What it identifies |
|---|---|---|
| Slug | `samsung-galaxy-m35-5g` | Human-readable, SEO only, not an ID |
| Item path | `/p/itm9d1f7a...` | The item page, an opaque hash |
| `pid` | `MOBGWFHZ8ZAABCDE` | The **product**: a specific model and variant |
| `lid` | `LSTMOBGWFHZ8ZAABCDE7XZKQF` | The **listing**: one seller's offer on that product |
| `marketplace` | `FLIPKART` | The marketplace context |
Here is the part almost every Flipkart tutorial gets wrong. The pid is a 16-character code that names a product variant, for example a specific phone in a specific colour and storage size. The lid names a single seller's listing of that product. One pid can carry several lid values because several sellers offer the same product, each at a different price with a different delivery promise. The lid usually embeds the pid (it starts with LST plus the pid), which is a handy sanity check. The leading letters of the pid are a category token: MOB for mobiles, COM for computers, TSH for t-shirts, and so on.
For price monitoring, decide up front what you are tracking. If you want "the price of this product", key on pid and record which lid won the buy box. If you want "this specific seller's price", key on the (pid, lid) pair. Pulling pid and lid out of a URL is a two-line job:
import re
from urllib.parse import urlparse, parse_qs
def flipkart_ids(url: str) -> dict:
q = parse_qs(urlparse(url).query)
pid = q.get("pid", [None])[0]
lid = q.get("lid", [None])[0]
# item hash from the /p/itm... path segment
m = re.search(r"/p/(itm[0-9a-z]+)", url)
return {"pid": pid, "lid": lid, "item": m.group(1) if m else None}
You can rebuild a canonical product URL from a bare pid alone (https://www.flipkart.com/x/p/itm?pid=), which is useful when you have collected pid values from search and want to revisit each product page directly.
Why Flipkart is hard to scrape
Flipkart runs a serious anti-bot stack behind a CDN, and it is built for Indian traffic. Four things break naive scrapers:
The bot wall. Flipkart has long fronted its traffic with Akamai, whose Bot Manager fingerprints the TLS handshake and the browser, then serves an "Access Denied" page or a challenge when a request looks automated. The trap is that a block can arrive as a plain HTTP 200 with an error shell instead of the product, so trusting response.ok stores garbage. If you want the mechanics of that defense, see How to Bypass Akamai Bot Manager.
The India geo gate. Flipkart serves India. Hit it from a US or EU datacenter IP and you get a degraded experience, wrong or missing pricing, redirects, or an outright block. To see correct INR pricing and real availability, you have to look like an Indian shopper, which means an Indian residential IP.
Hashed, rotating class names. Flipkart minifies its CSS classes to short hashes like ._30jeq3. Those hashes change on redeploys, so a scraper pinned to one class name goes silently blank on the next Flipkart release. This is worse than Amazon's occasional A/B test, because the churn is routine.
Pincode and login variance. Delivery, availability, and some offers depend on the 6-digit pincode you set and whether you are logged in. Scrape the same product from different sessions without pinning a location and your data jitters for reasons that have nothing to do with a real price change.
| Signal | What you will see | How to handle it |
|---|---|---|
| Bot wall (Akamai) | "Access Denied", a reference hash, or 200 with an error shell | Rotate residential IP, detect in the body, retry |
| India geo gate | Wrong currency, redirect, or block from a non-IN IP | `premium_proxy=true` plus `country_code=in` |
| Hashed class churn | A selector suddenly returns nothing | Candidate list plus structural and regex fallbacks |
| Pincode variance | Price, stock, or delivery differ run to run | Pin one pincode with a `js_scenario` |
| Rate limiting | 429s or throttling after bursts | Back off, keep concurrency modest |
A managed scraping API absorbs the first two rows and the last one for you. The hashed-class problem lives in the HTML, so it stays in your parser. The next sections handle each in turn.
Set up the SparkProxy Scraping API
The SparkProxy Scraping API takes a target URL and handles the proxy, rotation, browser rendering, and anti-bot layer for you. You send one request and get the rendered HTML back. For Flipkart, three parameters carry the weight:
render_js=true: Flipkart hydrates parts of the page with JavaScript, and rendering with a real Chromium browser gets the final DOM. It is the safe default.premium_proxy=true: routes the request through residential IPs, which survive Flipkart's defenses where datacenter IPs get flagged fast.country_code=in: sets the exit country to India (inis the ISO 3166-1 code for India). This is not optional for Flipkart. It gives you an Indian IP, INR pricing, and the availability a real shopper in India would see.
The base endpoint is https://scrape.sparkproxy.io/api/v1 and auth is a single header, X-API-Key. A minimal request in cURL:
curl -G "https://scrape.sparkproxy.io/api/v1" \
-H "X-API-Key: YOUR_API_KEY" \
--data-urlencode "url=https://www.flipkart.com/x/p/itm?pid=MOBGWFHZ8ZAABCDE" \
--data-urlencode "render_js=true" \
--data-urlencode "premium_proxy=true" \
--data-urlencode "country_code=in"
The full parameter list and response fields live in the Scraping API docs. If you are weighing this against running your own India proxy pool, Web Scraping API vs Self-Managed Proxies lays out the trade-off.
Scrape a single product by pid
Start with one product. Wrap the request so every call carries the Flipkart-specific parameters, and give it a generous timeout since a rendered request runs a real browser.
import requests
API = "https://scrape.sparkproxy.io/api/v1"
API_KEY = "YOUR_API_KEY"
def fetch(url: str) -> str:
resp = requests.get(
API,
headers={"X-API-Key": API_KEY},
params={
"url": url,
"render_js": "true", # Flipkart hydrates parts of the page
"premium_proxy": "true", # residential IPs survive the bot wall
"country_code": "in", # Indian IP, INR pricing, real stock
},
timeout=90,
)
resp.raise_for_status()
return resp.text
def product_url(pid: str) -> str:
return f"https://www.flipkart.com/x/p/itm?pid={pid}"
Before you trust the HTML, check whether Flipkart handed you a block instead of a product. Because a block can arrive as HTTP 200, raise_for_status() will not catch it. Scan the body for the telltale markers:
def is_blocked(html: str) -> bool:
"""A Flipkart or Akamai block can return HTTP 200, so the status code lies."""
markers = (
"Access Denied", # Akamai block page
"Reference ", # Akamai reference id
"Something's not right", # Flipkart error shell
"Retry", # Flipkart retry interstitial
"captcha",
"unusual traffic",
)
lowered = html.lower()
return any(m.lower() in lowered for m in markers) or len(html) < 2000
The length check matters. When Flipkart returns an empty React shell, the body is short and contains none of the product fields, so a size floor catches the "200 but empty" case that marker matching alone misses.
Parse the core fields
Flipkart's hashed class names are the enemy of a durable parser. The fix is a layered strategy: try a short list of known class candidates, and when they miss, fall back to something structural that does not depend on a hash. For parsing at scale use selectolax, a C-backed parser that chews through Flipkart's large DOM roughly an order of magnitude faster than the pure-Python default. Install it with pip install selectolax.
The single most durable Flipkart selector is not a class at all. The title lives in the og:title meta tag, and the price is always rendered as a rupee amount, so a regex over the buy-box region beats betting on a class hash:
import re
from selectolax.parser import HTMLParser
# Known class candidates as of mid-2026. Flipkart rotates these, so treat
# them as hints, not guarantees, and keep the fallbacks below.
TITLE_SELECTORS = ["span.VU-ZEz", "span.B_NuCI", "h1 span"]
PRICE_SELECTORS = ["div.Nx9bqj.CxhGGd", "div._30jeq3._16Jk6d", "div._30jeq3"]
RATING_SELECTORS = ["div.XQDdHH", "div._3LWZlK"]
PRICE_RE = re.compile(r"₹\s?([\d,]+)")
def _first_text(tree, selectors):
for sel in selectors:
node = tree.css_first(sel)
if node and node.text(strip=True):
return node.text(strip=True)
return None
def parse_product(html: str, url: str) -> dict:
tree = HTMLParser(html)
# Title: prefer the stable og:title meta, then fall back to hashed classes.
og = tree.css_first('meta[property="og:title"]')
title = og.attributes.get("content") if og else _first_text(tree, TITLE_SELECTORS)
# Price: try known classes, then regex the first rupee amount as a fallback.
price = _first_text(tree, PRICE_SELECTORS)
if not price:
m = PRICE_RE.search(html)
price = f"₹{m.group(1)}" if m else None
ids = flipkart_ids(url)
return {
"pid": ids["pid"],
"lid": ids["lid"],
"title": title,
"price": price,
"rating": _first_text(tree, RATING_SELECTORS),
}
The specifications table is Flipkart's richest structured section, and it is far more stable than the buy-box classes because it is built from plain A few things worth knowing. Flipkart writes counts in the Indian grouping system, so "1,20,431 Ratings" means 120,431. Strip commas before you cast to an integer, and do not assume Western thousands grouping. The rating badge is a bare number like Maintaining selectors across Flipkart's class churn is the tax on self-parsing. The SparkProxy Scraping API can do the extraction server-side with the The Here is the gotcha that quietly corrupts Flipkart price datasets: Flipkart localizes delivery, availability, and parts of the offer stack by the shopper's 6-digit pincode, and some prices and early-access deals depend on login and Flipkart Plus status. Scrape the same There are two layers to control: Country. Pincode, within India. Flipkart stores a delivery pincode and re-computes delivery and stock from it. To pin it, drive the pincode input with a browser automation scenario using the The pincode input selector is one of Flipkart's drift-prone ones, so confirm the current field in devtools if the scenario stops filling it. Pick one canonical pincode per run, a major metro like One more distinction that trips people up: the large number in the buy box is the selling price, but the "effective price" Flipkart advertises often bakes in a bank offer or coupon that only applies to specific cards. Record the selling price as your canonical number and capture the offer text separately, so you never conflate a conditional discount with the actual price. If you are building a price-comparison feed across retailers, this discipline is the whole game, and Datacenter Proxies for Price Comparison Websites goes into it. Search pages let you discover products by keyword. Rather than fight hashed result-card classes, anchor on structure: every product link on a Flipkart search page points at a To walk every page, request Like most marketplaces, Flipkart caps how deep search pagination goes, so a broad query such as "laptop" leaves most of the catalog unreachable past the cap. The fix is to narrow: split by brand, price band, or category filter, run each tight query to its page limit, then dedupe on At volume, three things keep the pipeline healthy: retries on soft blocks, backoff so you do not spike the service, and modest concurrency. With a scraping API the provider rotates the exit IP for you, so your ceiling is your plan's rate limit, not the number of Indian proxies you own. Keep worker counts sane, 5 to 12 is plenty, and let retries absorb the occasional block. The backoff matters more than it looks. Jitter ( For a running price tracker, write to a database keyed on Scraping publicly accessible pages (no login) generally does not trigger the US Computer Fraud and Abuse Act under hiQ v. LinkedIn (2022), but Flipkart is Indian, so Flipkart's Terms of Use and India's DPDP Act, 2023 are the frame that matters. Those Terms prohibit automated collection, so stick to public product data, never touch personal data about shoppers, avoid overloading the service, and get legal advice before any commercial use. The Flipkart fronts its traffic with Akamai Bot Manager, which can return an "Access Denied" page or a challenge, sometimes with HTTP 200, so you must scan the response body rather than trust the status code. Reduce triggers with rotating Indian residential IPs and a real browser: set Flipkart localizes delivery, availability, and parts of the offer stack by the 6-digit delivery pincode, and some deals and early access depend on login and Flipkart Plus status. Pin one pincode with a Yes. Pass the In practice, yes. Flipkart serves the Indian market, so a non-India IP can get wrong currency, degraded pricing, redirects, or a block. Setting 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 Scrape data with XPath and CSS selectors that survive redesigns: label-anchored XPath, stable attributes, namespace fixes, and a selector-drift detector. Stealth plugins patch the known automation tells in Puppeteer and Playwright. Which ones still matter, why the patch set is a signature, and how to test yours. Scrape Zomato and Swiggy data that means something: pin the delivery coordinates, key every row by restaurant, pin and timestamp, and split the fee stack. rows of label and value cells. Walk the tables and collect key-value pairs, which sidesteps the hash problem entirely:
def parse_specs(html: str) -> dict:
tree = HTMLParser(html)
specs = {}
for row in tree.css("table tr"):
cells = row.css("td")
if len(cells) >= 2:
key = cells[0].text(strip=True)
val = " ".join(c.text(strip=True) for c in cells[1:]).strip()
if key and val:
specs[key] = val
return specs # {"RAM": "8 GB", "Battery Capacity": "6000 mAh", ...}
4.4, not "4.4 out of 5", so store the scale yourself. And the seller name is tied to the lid, not the pid, so record it alongside the listing id. Get structured JSON with extract_rules
extract_rules parameter: you pass a map of field names to selectors, and the API returns JSON keyed by your names. This turns the scraping endpoint into a lightweight flipkart product data api where the response is already structured.import json
import requests
rules = {
"title": "meta[property='og:title']@content", # @attr pulls an attribute
"price": "div._30jeq3._16Jk6d",
"rating": "div._3LWZlK",
"seller": "#sellerName span span",
}
resp = requests.get(
"https://scrape.sparkproxy.io/api/v1",
headers={"X-API-Key": "YOUR_API_KEY"},
params={
"url": "https://www.flipkart.com/x/p/itm?pid=MOBGWFHZ8ZAABCDE",
"render_js": "true",
"premium_proxy": "true",
"country_code": "in",
"extract_rules": json.dumps(rules),
},
timeout=90,
)
data = resp.json() # {"title": "...", "price": "₹27,999", "rating": "4.4", ...}
@content suffix pulls an attribute instead of text, which is how you grab the title out of the og:title meta. The exact extract_rules syntax your plan exposes is in the docs, so confirm it there before you build on it. The trade is the same one from the previous section, just moved server-side: you do not ship a parser, but you still update the rules when Flipkart rotates its classes. For a small, fixed field set like price monitoring, extract_rules is usually the lower-maintenance path, and anchoring rules on meta tags and the spec table keeps them alive longer than buy-box hashes. Handle pincode and login price variance
pid from rotating IPs without pinning a pincode and your "price history" will wobble because the delivery estimate and stock changed, not the price.country_code=in puts you on an Indian IP so you see INR and Indian availability at all. Without it you are not measuring the Indian market, you are measuring whatever Flipkart shows a foreign visitor. For the wider theory on why the exit country changes what you see, see What Geo-Targeting Means in Proxies.js_scenario parameter, so every request in a batch localizes to the same pincode:import json
import requests
pincode_scenario = {
"instructions": [
{"click": "span:contains('Enter Delivery Pincode')"},
{"wait_for": "#pincodeInputId"},
{"fill": ["#pincodeInputId", "560001"]}, # Bengaluru
{"click": "span:contains('Check')"},
{"wait": 2},
]
}
resp = requests.get(
"https://scrape.sparkproxy.io/api/v1",
headers={"X-API-Key": "YOUR_API_KEY"},
params={
"url": "https://www.flipkart.com/x/p/itm?pid=MOBGWFHZ8ZAABCDE",
"render_js": "true",
"premium_proxy": "true",
"country_code": "in",
"js_scenario": json.dumps(pincode_scenario),
},
timeout=120,
)
560001 (Bengaluru) or 110001 (New Delhi), and hold it constant, so a price change in your data reflects a real price change and not a delivery-zone change. Scrape and paginate search results
/p/ path and carries a pid in its href. Read the anchors and pull the ids straight from the URL.from urllib.parse import quote_plus, urljoin
from selectolax.parser import HTMLParser
def search_url(query: str, page: int) -> str:
return f"https://www.flipkart.com/search?q={quote_plus(query)}&page={page}"
def parse_search(html: str) -> list[dict]:
tree = HTMLParser(html)
seen, rows = set(), []
for a in tree.css("a[href*='/p/']"):
href = a.attributes.get("href", "")
full = urljoin("https://www.flipkart.com", href)
ids = flipkart_ids(full)
if not ids["pid"] or ids["pid"] in seen:
continue
seen.add(ids["pid"])
rows.append({"pid": ids["pid"], "lid": ids["lid"], "url": full})
return rows
&page=N and stop when a page returns no new products:def scrape_search(query: str, max_pages: int = 20) -> list[dict]:
results = []
for page in range(1, max_pages + 1):
html = fetch(search_url(query, page))
if is_blocked(html):
continue
rows = parse_search(html)
if not rows:
break
results.extend(rows)
return results
pid. Ten targeted queries surface far more of the catalog than one broad sweep. Scale without getting blocked
import time
import random
from concurrent.futures import ThreadPoolExecutor, as_completed
def fetch_product(pid: str, attempts: int = 3) -> dict | None:
url = product_url(pid)
for i in range(attempts):
html = fetch(url)
if not is_blocked(html):
return parse_product(html, url)
time.sleep(2 ** i + random.random()) # exponential backoff + jitter
return None
def scrape_catalog(pids: list[str], workers: int = 8) -> list[dict]:
out = []
with ThreadPoolExecutor(max_workers=workers) as pool:
futures = {pool.submit(fetch_product, p): p for p in pids}
for fut in as_completed(futures):
row = fut.result()
if row:
out.append(row)
return out
random.random()) staggers retries so a batch of failures does not retry in lockstep and re-trigger the same throttle. Persist results as you go rather than holding everything in memory, so a crash at product 40,000 does not cost you the first 39,999. A flat CSV is enough to start, and stamping each row with the pincode and a timestamp gives you a clean, comparable time series:import csv
from datetime import datetime, timezone
def save_csv(rows: list[dict], pincode: str, path: str = "flipkart.csv") -> None:
if not rows:
return
now = datetime.now(timezone.utc).isoformat()
for r in rows:
r["pincode"] = pincode
r["scraped_at"] = now
with open(path, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
writer.writeheader()
writer.writerows(rows)
(pid, scraped_at) and keep the winning lid and pincode on every row. That gives you a time series where every price point is comparable because the location and the seller context were held constant. The same patterns apply across other marketplaces: our How to Scrape Amazon Product Data guide covers the ASIN and ZIP equivalents, and How to Scrape Ecommerce Prices generalizes the monitoring loop across sites. If you keep hitting blocks, How to Avoid Getting Your Proxy Blocked goes deeper on the proxy side.Frequently asked questions
FAQ
pid is a 16-character product identifier that names a specific model and variant, and it is your primary key. The lid is the listing identifier for one seller's offer on that product, and it usually starts with LST plus the pid. One pid can have several lid values when multiple sellers list the same item, so key on pid for the product and on (pid, lid) for a specific seller's price.render_js=true, premium_proxy=true, and country_code=in on the SparkProxy Scraping API, detect block markers in the body, and retry with exponential backoff.js_scenario that fills the delivery input, and record the plain selling price separately from conditional bank or coupon offers, so your price series reflects real changes rather than location or offer noise.extract_rules parameter to the SparkProxy Scraping API with a map of field names to CSS selectors, and the response comes back as JSON keyed by your names, which effectively gives you a flipkart product data api built on the scraping endpoint. Anchor rules on stable targets like the og:title meta tag and the specification table, since Flipkart's buy-box class names rotate.country_code=in with premium_proxy=true gives you an Indian residential exit so you see INR prices and the availability a real shopper in India would see.Related articles

XPath and CSS Selectors: Scrapers That Don't Break

Stealth Plugins for Puppeteer and Playwright: What Works

How to Scrape Zomato and Swiggy Data (Menus and Prices)
