DEV Community

Bugrahan Saka
Bugrahan Saka

Posted on

Swiggy Scraper in Python: Dish Prices and Menus by Location

Swiggy is India's biggest food delivery app, and its search answers the question every food business asks: what does this dish cost across the restaurants that deliver here? One search for "biryani" returns about 50 restaurants and every biryani on their menus, with price, veg flag, rating and delivery time. Getting that into a CSV takes one short Python script, but there are three things in the way: an AWS WAF challenge, a web page with no data in it, and results that change completely with the delivery address.

The block: an AWS WAF challenge

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://www.swiggy.com/city/bangalore", headers={"User-Agent": chrome})
print(r.status_code, len(r.text), r.headers.get("x-amzn-waf-action"))  # 202 0 challenge
Enter fullscreen mode Exit fullscreen mode
Request Result
requests with a Chrome user agent HTTP 202, empty body, x-amzn-waf-action: challenge
requests with its default user agent HTTP 403, an error page
Through Scrape.do, geoCode=in 200 in about 3 s, the full response, every try

The 202 is the tricky one: a status check that only looks for errors lets it through, and the script parses an empty string. I route every request through Scrape.do from India (geoCode=in). A plain request is enough here, 1 credit each.

Trap 2: the page has no data

A restaurant page like /city/bangalore/starbucks-coffee-ashok-nagar-rest124178 comes back as 78 KB of HTML with no menu in it. The browser fills it in afterwards from JSON endpoints under /dapi, so those are what the script calls:

Data Endpoint
Dish search /dapi/restaurants/search/v3?lat&lng&str=biryani&selectedPLTab=DISH
Restaurant search the same, with selectedPLTab=RESTAURANT
Full menu /dapi/menu/pl?page-type=REGULAR_MENU&complete-menu=true&lat&lng&restaurantId=…

Every response is a list of widget cards with an @type. The dish search returns one DishGroup card per restaurant, each holding the restaurant and its matching dishes; the rest are filters and suggestions. Money is in paise: "price": 42000 is ₹420, and some dishes only carry defaultPrice.

import csv
import json

import requests

TOKEN = "<your_token>"
LAT, LNG = 19.0760, 72.8777  # delivery location: Mumbai

def swiggy(path, **query):
    url = requests.Request("GET", "https://www.swiggy.com" + path, params=query).prepare().url
    response = requests.get("https://api.scrape.do/",
                            params={"token": TOKEN, "url": url, "geoCode": "in"}, timeout=60)
    body = response.json()
    if response.status_code != 200 or body.get("statusCode") != 0:
        raise RuntimeError(f"HTTP {response.status_code}: {body.get('statusMessage')}")
    return body["data"]

def rupees(paise):
    return int(paise) / 100 if paise is not None else None

data = swiggy("/dapi/restaurants/search/v3", lat=LAT, lng=LNG, str="biryani",
              trackingId="undefined", submitAction="ENTER", selectedPLTab="DISH")
cards = data["cards"][0]["groupedCard"]["cardGroupMap"]["DISH"]["cards"]

rows = []
for card in cards:
    group = card["card"]["card"]
    if not group.get("@type", "").endswith("DishGroup"):
        continue  # filters and query suggestions
    shop = group["restaurant"]["info"]
    for dish in group["dishes"]:
        item = dish["info"]
        rows.append({
            "dish": item["name"],
            "price_inr": rupees(item.get("price", item.get("defaultPrice"))),
            "veg": bool(item.get("isVeg")),
            "rating": item.get("ratings", {}).get("aggregatedRating", {}).get("rating"),
            "restaurant": shop["name"],
            "restaurant_rating": shop.get("avgRating"),
            "delivery_min": shop["sla"].get("deliveryTime"),
            "ad": bool(shop.get("promoted")),
        })

with open("swiggy-biryani.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)} dishes from {len({r['restaurant'] for r in rows})} restaurants")
Enter fullscreen mode Exit fullscreen mode

One request, 304 dishes from 50 restaurants. 13 of those dishes came from restaurants Swiggy marks as promoted: paid placements mixed into the results, worth a column of their own.

Trap 3: the address decides the results

lat and lng decide which restaurants deliver, how far they are and how long delivery takes. The same search in two cities:

"biryani" Bengaluru Mumbai
Restaurants in the dish search 50 50
Restaurants in the restaurant search 102 100
Cheapest dish with "biryani" in its name ₹90 ₹80

Not one restaurant appears in both lists. Change the coordinates and you get a different market, so record them with every row. A menu still loads for a far-away address; the restaurant comes back with serviceability: NON_SERVICEABLE instead of a delivery time.

The full menu: variants and FSSAI licence

The menu endpoint returns every section of a restaurant's menu, plus its FSSAI licence and address. One catch: the "Recommended" section repeats dishes from the other sections. Starbucks in Ashok Nagar has 168 dish cards and 148 dishes. Same script, continued:

import re

def menu(restaurant_id):
    data = swiggy("/dapi/menu/pl", **{"page-type": "REGULAR_MENU", "complete-menu": "true"},
                  lat=LAT, lng=LNG, restaurantId=restaurant_id)
    sections = next(c for c in data["cards"] if "groupedCard" in c)["groupedCard"]["cardGroupMap"]["REGULAR"]["cards"]
    dishes, licence = {}, None
    for section in sections:
        card = section["card"]["card"]
        if card.get("@type", "").endswith("RestaurantLicenseInfo"):
            licence = re.search(r"\d{10,}", " ".join(card["text"])).group(0)
        nested = [card] + card.get("categories", [])
        for group in nested:
            for item_card in group.get("itemCards", []):
                item = item_card["card"]["info"]
                dishes.setdefault(item["id"], {  # "Recommended" repeats dishes: keep each once
                    "dish": item["name"],
                    "category": item.get("category"),
                    "price_inr": rupees(item.get("price", item.get("defaultPrice"))),
                    "veg": bool(item.get("isVeg")),
                    "in_stock": bool(item.get("inStock", 1)),
                })
    return licence, list(dishes.values())

licence, dishes = menu("124178")  # Starbucks, Ashok Nagar, Bengaluru
print(f"FSSAI {licence}: {len(dishes)} dishes")
print(dishes[0])
Enter fullscreen mode Exit fullscreen mode
FSSAI 11218333002406: 148 dishes
{'dish': 'Marigold Oat cold brew', 'category': 'Diwali Specials', 'price_inr': 420.0, 'veg': True, 'in_stock': True}
Enter fullscreen mode Exit fullscreen mode

Sizes and options sit in variants (a surcharge on the base price) or variantsV2.pricingModels (absolute prices); the package below turns both into rupees.

Skip the boilerplate

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

pip install swiggy-scraper
export SCRAPEDO_TOKEN=your_token
swiggy-scraper dishes biryani --city mumbai -f csv -o swiggy-biryani.csv
swiggy-scraper restaurants pizza --city bengaluru -f csv -o swiggy-pizza.csv
swiggy-scraper menu https://www.swiggy.com/city/bangalore/starbucks-coffee-ashok-nagar-rest124178 -f csv -o swiggy-menu.csv
Enter fullscreen mode Exit fullscreen mode

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

Run it at scale with Scrape.do

Every request here is one Scrape.do call from India, 1 credit: no browser, no WAF tokens. Run the same dish search for every neighbourhood you care about and you have a price map of the market, or pull the menus of your competitors every morning. Get 1000 free credits and start scraping with Scrape.do.

The script calls the JSON endpoints Swiggy's own pages use for search and menus. Swiggy's robots.txt allows these paths and disallows account, cart, checkout, order tracking and payment pages, which it never touches. Restaurant and menu data is not personal data; read Swiggy's terms for your use case and keep request rates modest.

Top comments (0)