Skip to content

Case Studies & ProjectsAPI Researcher

FoodPanda Pakistan — Mapping a Private API and Building a Deal Scanner

By Burhan Moin

Motivation

FoodPanda Pakistan runs limited-time flash deals that go live at unpredictable times and sell out within minutes. The official app provides no notification system for these deals beyond generic push alerts. I wanted a way to be alerted the instant a deal matching my criteria went live — and to query the full restaurant and menu catalogue in ways the app UI does not allow.

Traffic Interception Setup

I set up Charles Proxy on macOS with SSL proxying enabled and installed the Charles root certificate on an Android device running the FoodPanda app. This gave me full visibility into every HTTPS request the app made.

text·setup.txt
Environment:
  Proxy:   Charles Proxy 4.x (macOS)
  Device:  Android 13, certificate trust installed
  Target:  FoodPanda Pakistan (pk.fd-api.com)

SSL Proxying hosts:
  *.fd-api.com
  *.foodpanda.pk

API Surface Mapping

Authentication flow

The app authenticates with a POST to /api/v5/oauth/token using a username/password grant. The response includes a bearer token with a 30-day expiry. All subsequent requests carry Authorization: Bearer <token>.

python·auth.py
import requests

BASE = "https://pk.fd-api.com"

def login(email: str, password: str) -> str:
    r = requests.post(f"{BASE}/api/v5/oauth/token", json={
        "grant_type":    "password",
        "username":      email,
        "password":      password,
        "client_id":     "panda",          # hardcoded in APK
        "client_secret": "...",            # extracted from APK resources
    })
    return r.json()["access_token"]

Deals endpoint

Flash deals are served from /api/v5/vendors with a specific campaign_id parameter that the app populates when the deals banner is active. By polling this endpoint I could detect new deals before the push notification system delivered them.

python·deals.py
def get_live_deals(token: str, lat: float, lon: float) -> list:
    r = requests.get(f"{BASE}/api/v5/vendors", headers=auth(token), params={
        "latitude":    lat,
        "longitude":   lon,
        "language_id": 1,
        "dynamic_pricing": 0,
        "configuration": "Panda",
        "country_code": "PK",
        "use_new_discovery": True,
        "campaign_id": "promotion",    # deals filter
    })
    vendors = r.json().get("data", {}).get("items", [])
    return [v for v in vendors if v.get("is_in_promotion")]

Full menu and price-range query

The /api/v5/vendors/{vendor_id}/menus endpoint returns the complete menu tree including every item, variant, price, and availability flag. I wrapped this into a search function that queries across any number of restaurants simultaneously.

python·menu_search.py
def search_menu(token: str, vendor_ids: list[str],
                query: str = "", min_price: float = 0, max_price: float = 9999) -> list:
    results = []
    for vid in vendor_ids:
        r = requests.get(f"{BASE}/api/v5/vendors/{vid}/menus",
                         headers=auth(token))
        for category in r.json().get("data", {}).get("product_groups", []):
            for item in category.get("products", []):
                price = item.get("product_variations", [{}])[0].get("price", 0)
                if (query.lower() in item["name"].lower()
                        and min_price <= price <= max_price):
                    results.append({
                        "restaurant": vid, "name": item["name"],
                        "price": price, "available": item.get("is_available"),
                    })
    return sorted(results, key=lambda x: x["price"])

The Deal Scanner CLI

I combined everything into a persistent CLI daemon that polls the deals endpoint every 30 seconds and sends a desktop notification (macOS osascript) the moment a deal matching my filter list appears.

python·scanner.py
import time, subprocess

POLL_INTERVAL = 30  # seconds
KEYWORDS      = ["pizza", "burger", "biryani"]

def notify(restaurant: str, deal: str):
    subprocess.run(["osascript", "-e",
        f'display notification "{deal}" with title "FoodPanda Deal: {restaurant}"'])

def run(token, lat, lon):
    seen = set()
    while True:
        deals = get_live_deals(token, lat, lon)
        for d in deals:
            key = d["id"]
            if key not in seen:
                seen.add(key)
                name = d.get("name", "")
                if any(k in name.lower() for k in KEYWORDS):
                    notify(name, d.get("tag", "New deal live"))
        time.sleep(POLL_INTERVAL)

Results

The scanner has been running for several months. I have been alerted to deals averaging 2–4 minutes earlier than the official push notifications. The menu search function covered over 800 restaurants across Karachi in a single query — returning results sorted by price in under 3 seconds.

[ Screenshots of deal alerts and CLI output to be added here ]

Key Takeaways

The FoodPanda API has no additional rate-limiting or anomaly detection beyond the standard bearer-token authentication. Client credentials embedded in the APK (client_id, client_secret) are static and not rotated. Any actor with Burp Suite or Charles and 20 minutes can fully map this API surface. Proper mitigation requires certificate pinning, rotating client credentials, and server-side anomaly detection on request patterns.