DEV Community

Bugrahan Saka
Bugrahan Saka

Posted on

Blinkit Scraper in Python: Prices and Stock per Dark Store

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
Enter fullscreen mode Exit fullscreen mode
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']}")
Enter fullscreen mode Exit fullscreen mode

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"]))
Enter fullscreen mode Exit fullscreen mode
{'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'}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)