zepto-scraper
A Python scraper for Zepto, India's quick-commerce grocery app. Two jobs:
- Category listings: start from one subcategory URL, walk its sibling subcategories and get every product card with price, MRP, discount, in-stock flag, units in stock, rating and the dark store it came from.
- Product details: turn product pages into a catalog sheet with seller entity, shelf life, FSSAI licence number, country of origin, category path, order limit and the full description.
Output as Python dicts, JSON or CSV. One dependency (requests), no browser.
pip install zepto-scraper
Why a plain request fails
Zepto sits behind AWS WAF on CloudFront:
| Request | Status | Body |
|---|---|---|
requests, default user agent |
429 | 0 bytes |
requests, Chrome user agent |
202, x-amzn-waf-action: challenge |
0 bytes |
| Through Scrape.do, no parameters | 200 | full page |
Watch the 202. It means "Accepted", so raise_for_status() treats it as success and a scraper that only checks for errors writes empty pages without a single failure. This package treats anything but a 200 as an error.
Pages are fetched through the Scrape.do API with a plain request: no render, no super, no geoCode, 1 credit per page. You need a Scrape.do API token; the free plan includes 1,000 credits a month.
Quick start
from zepto_scraper import ZeptoScraper
scraper = ZeptoScraper(token="YOUR_SCRAPEDO_TOKEN") # or set SCRAPEDO_TOKEN
# Any subcategory URL; the scraper walks its siblings in the same category
rows = scraper.get_category(
"https://www.zepto.com/cn/munchies/chips-crisps/cid/d2c2a144-43cd-43e5-b308-92628fa68596/scid/df4f5100-c02f-4906-83b8-ddb744081a7a"
)
for row in rows[:3]:
print(row["name"], row["price_inr"], row["mrp_inr"], row["in_stock"], row["available_qty"])
product = scraper.get_product(rows[0]["url"])
print(product["seller"], product["shelf_life"], product["fssai_licence"])
Start from a subcategory URL (/cn/<category>/<subcategory>/cid/.../scid/...). The category link on Zepto's homepage is not a listing.
Command line
export SCRAPEDO_TOKEN=your_token
# every product in a category, CSV
zepto-scraper category "<subcategory-url>" -f csv -o zepto-category.csv
# catalog sheet for the first 10 products from that file
zepto-scraper product -i zepto-category.csv --limit 10 -f csv -o zepto-products.csv
The two commands chain: product reads the url column of the listing CSV.
Output
Listing row (from the fictional test page in tests/fixtures):
{
"variant_id": "44444444-4444-4444-8444-444444444440",
"name": "Example Chilli Chips",
"brand": "Examplo",
"pack_size": "1 pack (41 g)",
"price_inr": 19.0,
"mrp_inr": 20.0,
"discount_pct": 5,
"in_stock": true,
"available_qty": 30,
"rating": 4.9,
"rating_count": 401062,
"subcategory": "Chips & Crisps",
"store_id": "33333333-3333-4333-8333-333333333333",
"url": "https://www.zepto.com/pn/example-snack-0/pvid/44444444-4444-4444-8444-444444444440"
}
Product sheet columns: variant_id, name, brand, category, pack_size, mrp_inr, max_per_order, rating, rating_count, shelf_life, country_of_origin, seller, fssai_licence, image_count, description, url.
How it works
The Next.js stream. Zepto runs on the Next.js App Router. A subcategory page makes no separate product API call; every product card is streamed into the HTML as self.__next_f.push([1, "..."]) chunks. Each chunk is a JavaScript string literal, and JSON strings use the same escaping, so json.loads('"' + chunk + '"') decodes it. Joined, the chunks are one text with one JSON object per product card.
Out-of-stock cards. Card keys are sorted alphabetically. In-stock cards start with availableQuantity; out-of-stock cards drop that key and start with baseProductId. A parser that anchors only on availableQuantity silently skips every out-of-stock product. This one anchors on both, reads each object with json.JSONDecoder.raw_decode, and dedupes on the variant id.
Prices are in paise. "discountedSellingPrice": 1900 is ₹19. The scraper divides by 100.
Siblings. Every subcategory page links its siblings, all sharing the category cid. The same subcategory can appear under a second category slug that renders no products, so only paths under the start URL's own slug are kept.
Product pages. The catalog fields come from the schema.org Product JSON-LD block (Zepto writes id before type on the script tag, so the regex does not depend on attribute order). MRP and the order limit come from the stream's productVariant object for the exact variant in the URL.
Limits
- 32 products per subcategory. Zepto's web pages stop at 32 cards (
endOfPage: true) and ignore?page=2. For wider coverage, walk more subcategories and categories. - One dark store. Each price and stock count belongs to one dark store. With no location, Zepto serves its default store (east Bengaluru); the
store_idcolumn records which one. Changing the location through Scrape.do did not move the store in our tests. - Use the listing for price and stock. Product pages cap the stock count and can lag the listing, so
get_product()leaves price and stock out on purpose. - No search. Search results need a browser-computed
request-signatureheader, and robots.txt disallows/search. Walk categories instead. - Errors. Non-200 responses (including the 202 challenge) raise
ScrapeErrorwith the status code. Server errors and 429s are retried twice; client errors are not.
Responsible use
The scraper reads category (/cn/) and product (/pn/) pages, which Zepto's robots.txt allows; it does not touch /search, /api/, cart or account paths. Product data is not personal data. Read Zepto's terms for your use case and keep request rates modest. This is not legal advice.
Development
git clone https://github.com/bugrahansss/zepto-scraper
cd zepto-scraper
pip install -e .
python -m unittest discover -s tests -v
The tests run offline against fictional fixture pages.
License
MIT
Built and tested with Scrape.do, October 2026.
Metadata
Release files for zepto-scraper 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| zepto_scraper-0.1.0.tar.gz | 16.6 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| zepto_scraper-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 28.9 kB
Release files / zepto_scraper-0.1.0.tar.gz
| Download URL | zepto_scraper-0.1.0.tar.gz |
|---|---|
| Size | 16.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8cf41e91719c52c06ec29c166cd92462d46fbe2ef73e7c6710ba3363e29755a3
|
|
BLAKE2b-256 checksum How to use checksums |
4dd306155e313ace361de7337c6ac58e06df9fd561f0a70612a1989e99672ebc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Release files / zepto_scraper-0.1.0-py3-none-any.whl
| Download URL | zepto_scraper-0.1.0-py3-none-any.whl |
|---|---|
| Size | 12.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
37d7717f73f4928848d8d94050e9ff9a81447527e2e2fad04bebf2d9f90b9952
|
|
BLAKE2b-256 checksum How to use checksums |
7bb20244d748e8a74c1aae04c525fd8796cacbcae89507ddf154af23a66c721c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Security for zepto-scraper 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.