Blinkit is India's 10-minute grocery app, and every product card on its website carries the numbers a price tracker wants: selling price, MRP, discount, rating and the number of units the dark store holds. Getting a whole category into a CSV takes one Python script, but there are three traps on the way: a Cloudflare wall, prices that depend on where you are, and a category that only shows 30 products before switching to a POST API.

The block: a Cloudflare 403

import requests 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" r = requests.get("https://blinkit.com/cn/chips-crisps/cid/1237/940", headers={"User-Agent": chrome}) print(r.status_code) # 403 
Request Result
requests, default or Chrome user agent HTTP 403, "blinkit | Error Page" (Cloudflare)
Through Scrape.do, super=true&geoCode=in 200 in about 3 s, full page, 5 of 5 tries

I route every request through Scrape.do with residential proxies in India (super=true&geoCode=in). Blinkit requests cost 10 credits each. The pagination API below only answers on residential proxies; a plain request to it fails.

The data: one JSON object per page

Blinkit renders on the server and puts all the data into one inline script, window.grofers.PRELOADED_STATE = {...}. A category page keeps its product cards in ui.plpContainer.feedData.snippets; each card has an add_to_cart payload with price, MRP, pack size, brand, store id and inventory, the units on the shelf (Blinkit stops counting at 50). Rating counts arrive in Indian units: "7 lac" is 700,000.

Trap 2: location picks the store

Without a delivery address Blinkit uses Gurugram. Every price, every stock count and even the list of products belongs to the dark store that serves your location. Same category, two cities:

Gurugram Mumbai
Store id 31719 41649
Products in "Chips & Crisps" 673 524
Overlap on the first page of 15 8 8
Units of one banana chips pack 5 1

The page reads the location from Blinkit's own cookies, gr_1_lat and gr_1_lon (Scrape.do sets them with setCookies). The pagination API reads it from lat and lon headers.

Trap 3: 30 products, then a POST API

The HTML holds 30 cards and a next_url for /v1/layout/listing_widgets. That endpoint is a POST, returns 15 products per call (a bigger limit is ignored) and hands back the next next_url. Scrape.do forwards the method and the body, and custom headers with extraHeaders=true and an sd- prefix.

import csv import json import re import requests TOKEN = "<your_token>" LAT, LON = 19.0760, 72.8777 # delivery location: Mumbai  def fetch(url, method="GET", headers=None, cookies=None): params = {"token": TOKEN, "url": url, "super": "true", "geoCode": "in"} if headers: params["extraHeaders"] = "true" # forward the sd- headers to Blinkit  headers = {f"sd-{k}": v for k, v in headers.items()} if cookies: params["setCookies"] = cookies response = requests.request(method, "https://api.scrape.do/", params=params, headers=headers, data="{}" if method == "POST" else None, timeout=120) if response.status_code != 200: raise RuntimeError(f"HTTP {response.status_code} for {url}") return response STATE_RE = re.compile(r"window\.grofers\.PRELOADED_STATE\s*=\s*") def extract_state(html): match = STATE_RE.search(html) if not match: return {} data, _ = json.JSONDecoder().raw_decode(html, match.end()) return data MULT = {"k": 1_000, "lac": 100_000, "cr": 10_000_000} def count(text): # "7 lac" -> 700000  m = re.search(r"([\d.,]+)\s*([a-z]*)", (text or "").lower()) return int(float(m.group(1).replace(",", "")) * MULT.get(m.group(2), 1)) if m else None def to_row(card): item = card["data"]["atc_action"]["add_to_cart"]["cart_item"] bar = (card["data"].get("rating") or {}).get("bar") or {} return { "product_id": item["product_id"], "name": item["product_name"], "brand": item["brand"], "pack_size": item["unit"], "price_inr": item["price"], "mrp_inr": item["mrp"], "inventory": card["data"].get("inventory", 0), "in_stock": not card["data"].get("is_sold_out"), "rating": round(bar["value"], 2) if bar.get("value") else None, "rating_count": count((bar.get("title") or {}).get("text")), "store_id": item["merchant_id"], } def cards(snippets): return [s for s in snippets if s.get("widget_type") == "product_card_snippet_type_2"] CATEGORY_URL = "https://blinkit.com/cn/chips-crisps/cid/1237/940" PAGES = 3 # Page 1: the HTML, with the location in Blinkit's own cookies. html = fetch(CATEGORY_URL, cookies=f"gr_1_lat={LAT}; gr_1_lon={LON}").text feed = extract_state(html)["ui"]["plpContainer"]["feedData"] rows = {r["product_id"]: r for r in map(to_row, cards(feed["snippets"]))} next_url = feed["pagination"]["next_url"] # Pages 2+: POST to the listing API, 15 products per call, location in headers. headers = {"lat": str(LAT), "lon": str(LON), "app_client": "consumer_web", "Content-Type": "application/json", "Accept": "application/json"} for _ in range(PAGES - 1): if not next_url: break data = fetch("https://blinkit.com" + next_url, method="POST", headers=headers).json()["response"] for row in map(to_row, cards(data["snippets"])): rows.setdefault(row["product_id"], row) next_url = (data.get("pagination") or {}).get("next_url") rows = list(rows.values()) with open("blinkit-chips.csv", "w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=list(rows[0].keys())) writer.writeheader() writer.writerows(rows) print(f"Saved {len(rows)} products from store {rows[0]['store_id']}") 

Three requests, 30 credits, 60 products from store 41649. A whole category of 673 products is 44 requests.

The product sheet: FSSAI licence and shelf life

The product page (/prn/<name>/prid/<id>) keeps a catalog sheet in ui.pdp.bffPdp.bffData.tracking.le_meta.custom_data.seo: price, MRP, stock and about 80 attributes, from ingredients and manufacturer to the FSSAI licence, shelf life, HSN code and return policy. Same script, continued:

def product_sheet(product_id): html = fetch(f"https://blinkit.com/prn/product/prid/{product_id}", cookies=f"gr_1_lat={LAT}; gr_1_lon={LON}").text bff = extract_state(html)["ui"]["pdp"]["bffPdp"]["bffData"] seo = bff["tracking"]["le_meta"]["custom_data"]["seo"] attrs = {a["name"]: a["value"] for a in seo["attributes"]} return { "name": seo["product_name"], "price_inr": seo["price"], "mrp_inr": seo["mrp"], "inventory": seo["inventory"], "fssai_license": attrs.get("FSSAI License"), "shelf_life": attrs.get("Shelf Life"), "country_of_origin": attrs.get("Country of Origin"), } print(product_sheet(rows[0]["product_id"])) 
{'name': 'Unibic Swaadesi Super Spicy Banana Chips', 'price_inr': 43, 'mrp_inr': 70, 'inventory': 2, 'fssai_license': '11222999000012', 'shelf_life': '180 days', 'country_of_origin': 'India'} 

Skip the boilerplate

The same logic is packaged as an open-source library, blinkit-scraper on GitHub and PyPI, with city presets, retries and CSV output:

pip install blinkit-scraper export SCRAPEDO_TOKEN=your_token blinkit-scraper category "https://blinkit.com/cn/chips-crisps/cid/1237/940" --pages 3 --city mumbai -f csv -o blinkit-chips.csv blinkit-scraper product -i blinkit-chips.csv --limit 10 --city mumbai -f csv -o blinkit-products.csv 

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

Run it at scale with Scrape.do

Every request here is one Scrape.do call through residential proxies in India: no browser, no proxy setup. Run the same category for every city you sell in and you have store-level price and stock tracking. Get 1000 free credits and start scraping with Scrape.do.

The scripts read category (/cn/) and product (/prn/) pages, which Blinkit's robots.txt allows, plus the listing API those pages call. They never touch search, cart, checkout or account paths. Product data is not personal data; read Blinkit's terms for your use case and keep request rates modest.