Tracking Food Delivery Promotions and Voucher Codes Automatically

Tracking Food Delivery Promotions and Voucher Codes Automatically

Food delivery platforms in Southeast Asia run an extraordinary number of promotions. On any given day, GrabFood, Foodpanda, ShopeeFood, and GoFood collectively offer thousands of voucher codes, flash sales, free delivery deals, and cashback promotions. For restaurants, cloud kitchens, and F&B analysts, manually tracking this promotional landscape is impossible.

This guide shows you how to build an automated system for tracking food delivery promotions and voucher codes across SEA markets.

The Promotion Landscape in SEA Food Delivery

Types of Promotions

Food delivery platforms use diverse promotional mechanics:

Promotion TypeDescriptionExample
Platform vouchersCodes issued by the platform“FREE5” for $5 off
Restaurant vouchersMerchant-funded discounts20% off at specific restaurant
Free deliveryWaived delivery feeFree delivery over $15
Bundle dealsDiscounted item combosFamily meal set at 30% off
Flash salesTime-limited discounts50% off from 2-4 PM
CashbackPoints or coins returned10x GrabRewards points
New user dealsFirst-order discounts50% off first 3 orders
Payment promotionsCard-specific discountsExtra 10% off with DBS card

Why Track Promotions?

For restaurants: Understanding competitor promotions helps you time your own deals and avoid being undercut during peak periods.

For cloud kitchens: Promotions directly impact order volume and margin. Knowing what deals are active helps with demand forecasting.

For analysts: Promotional spending patterns reveal platform strategies, competitive dynamics, and market maturation signals.

For consumers/deal sites: Aggregating active deals creates valuable content that drives traffic.

Building the Promotion Tracker

Architecture Overview

[Scraping Agents] --> [Promotion Parser] --> [Database]
                                                |
                                          [Analysis Engine]
                                                |
                                    [Alerts] [Dashboard] [Reports]

Core Tracker Implementation

import requests
import time
import random
import json
import hashlib
from datetime import datetime, timedelta
from dataclasses import dataclass, field
from typing import List, Optional

@dataclass
class Promotion:
    platform: str
    country: str
    promo_id: str
    title: str
    description: str
    promo_type: str  # voucher, free_delivery, cashback, bundle, flash_sale
    discount_type: str  # percentage, fixed, free_delivery
    discount_value: float
    min_order: float
    max_discount: Optional[float]
    voucher_code: Optional[str]
    start_time: Optional[datetime]
    end_time: Optional[datetime]
    applicable_restaurants: List[str] = field(default_factory=list)
    payment_methods: List[str] = field(default_factory=list)
    usage_limit: Optional[int] = None
    is_new_user_only: bool = False
    discovered_at: datetime = field(default_factory=datetime.utcnow)

    @property
    def is_active(self):
        now = datetime.utcnow()
        if self.start_time and now < self.start_time:
            return False
        if self.end_time and now > self.end_time:
            return False
        return True

    @property
    def fingerprint(self):
        """Unique identifier for deduplication."""
        key = f"{self.platform}:{self.country}:{self.voucher_code or self.title}"
        return hashlib.md5(key.encode()).hexdigest()

class PromotionTracker:
    def __init__(self, proxy_user, proxy_pass):
        self.proxy_user = proxy_user
        self.proxy_pass = proxy_pass
        self.known_promotions = {}

    def _get_session(self, country):
        session = requests.Session()
        proxy_host = f"{country.lower()}-mobile.dataresearchtools.com"
        session.proxies = {
            "http": f"http://{self.proxy_user}:{self.proxy_pass}@{proxy_host}:8080",
            "https": f"http://{self.proxy_user}:{self.proxy_pass}@{proxy_host}:8080"
        }
        session.headers.update({
            "User-Agent": "Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36",
            "Accept": "application/json"
        })
        return session

Scraping GrabFood Promotions

def scrape_grabfood_promotions(self, country="SG"):
    """Scrape active promotions from GrabFood."""
    session = self._get_session(country)
    promotions = []

    # Default coordinates per country
    coords = {
        "SG": (1.3521, 103.8198),
        "MY": (3.1390, 101.6869),
        "TH": (13.7563, 100.5018),
        "PH": (14.5995, 120.9842),
        "ID": (-6.2088, 106.8456)
    }

    lat, lng = coords.get(country, coords["SG"])

    # Fetch platform-level promotions
    response = session.get(
        "https://food.grab.com/api/v1/promotions",
        params={"lat": lat, "lng": lng}
    )

    if response.status_code == 200:
        data = response.json()
        for promo in data.get("promotions", []):
            promotions.append(Promotion(
                platform="grabfood",
                country=country,
                promo_id=promo.get("id", ""),
                title=promo.get("title", ""),
                description=promo.get("description", ""),
                promo_type=self._classify_promo_type(promo),
                discount_type=promo.get("discount_type", "unknown"),
                discount_value=promo.get("discount_value", 0),
                min_order=promo.get("min_order", 0),
                max_discount=promo.get("max_discount"),
                voucher_code=promo.get("voucher_code"),
                start_time=self._parse_datetime(promo.get("start_time")),
                end_time=self._parse_datetime(promo.get("end_time")),
                applicable_restaurants=promo.get("restaurant_ids", []),
                is_new_user_only=promo.get("new_user_only", False)
            ))

    # Also scan restaurant-level promotions from listings
    restaurant_response = session.get(
        "https://food.grab.com/api/v1/restaurants",
        params={"lat": lat, "lng": lng, "limit": 50}
    )

    if restaurant_response.status_code == 200:
        restaurants = restaurant_response.json().get("restaurants", [])
        for restaurant in restaurants:
            for promo in restaurant.get("promotions", []):
                promotions.append(Promotion(
                    platform="grabfood",
                    country=country,
                    promo_id=f"rest-{restaurant['id']}-{promo.get('id', '')}",
                    title=promo.get("title", ""),
                    description=promo.get("description", ""),
                    promo_type="restaurant_voucher",
                    discount_type=promo.get("type", "percentage"),
                    discount_value=promo.get("value", 0),
                    min_order=promo.get("min_spend", 0),
                    max_discount=promo.get("cap"),
                    voucher_code=promo.get("code"),
                    applicable_restaurants=[restaurant["id"]]
                ))

    time.sleep(random.uniform(2, 4))
    return promotions

Scraping Foodpanda Promotions

def scrape_foodpanda_promotions(self, country="SG"):
    """Scrape active promotions from Foodpanda."""
    session = self._get_session(country)
    promotions = []

    domains = {
        "SG": "www.foodpanda.sg",
        "MY": "www.foodpanda.my",
        "TH": "www.foodpanda.co.th",
        "PH": "www.foodpanda.ph"
    }
    domain = domains.get(country, domains["SG"])

    coords = {
        "SG": (1.3521, 103.8198),
        "MY": (3.1390, 101.6869),
        "TH": (13.7563, 100.5018),
        "PH": (14.5995, 120.9842)
    }
    lat, lng = coords.get(country, coords["SG"])

    # Fetch voucher listings
    response = session.get(
        f"https://{domain}/api/v5/vouchers",
        params={"latitude": lat, "longitude": lng}
    )

    if response.status_code == 200:
        data = response.json()
        for voucher in data.get("data", {}).get("items", []):
            promotions.append(Promotion(
                platform="foodpanda",
                country=country,
                promo_id=voucher.get("id", ""),
                title=voucher.get("title", ""),
                description=voucher.get("description", ""),
                promo_type=self._classify_foodpanda_promo(voucher),
                discount_type=voucher.get("discount_type", "unknown"),
                discount_value=voucher.get("value", 0),
                min_order=voucher.get("minimum_order_value", 0),
                max_discount=voucher.get("maximum_discount_amount"),
                voucher_code=voucher.get("code"),
                start_time=self._parse_datetime(voucher.get("valid_from")),
                end_time=self._parse_datetime(voucher.get("valid_until")),
                payment_methods=voucher.get("payment_types", [])
            ))

    # Scan promotional banners
    banner_response = session.get(
        f"https://{domain}/api/v5/campaigns",
        params={"latitude": lat, "longitude": lng}
    )

    if banner_response.status_code == 200:
        campaigns = banner_response.json().get("data", [])
        for campaign in campaigns:
            if campaign.get("voucher_code"):
                promotions.append(Promotion(
                    platform="foodpanda",
                    country=country,
                    promo_id=f"campaign-{campaign.get('id')}",
                    title=campaign.get("title", ""),
                    description=campaign.get("subtitle", ""),
                    promo_type="campaign",
                    discount_type="unknown",
                    discount_value=0,
                    min_order=0,
                    voucher_code=campaign.get("voucher_code")
                ))

    time.sleep(random.uniform(2, 4))
    return promotions

Promotion Analysis

Tracking Promotion Frequency

def analyze_promotion_frequency(promotion_history, days=30):
    """Analyze how often platforms run promotions."""
    cutoff = datetime.utcnow() - timedelta(days=days)
    recent = [p for p in promotion_history if p.discovered_at >= cutoff]

    by_platform = {}
    for promo in recent:
        key = f"{promo.platform}_{promo.country}"
        if key not in by_platform:
            by_platform[key] = {
                "total_promotions": 0,
                "by_type": {},
                "avg_discount_value": [],
                "voucher_codes": set()
            }

        by_platform[key]["total_promotions"] += 1

        ptype = promo.promo_type
        by_platform[key]["by_type"][ptype] = by_platform[key]["by_type"].get(ptype, 0) + 1

        if promo.discount_value > 0:
            by_platform[key]["avg_discount_value"].append(promo.discount_value)

        if promo.voucher_code:
            by_platform[key]["voucher_codes"].add(promo.voucher_code)

    # Calculate summaries
    for key, data in by_platform.items():
        values = data["avg_discount_value"]
        data["avg_discount_value"] = round(sum(values) / len(values), 2) if values else 0
        data["unique_voucher_codes"] = len(data["voucher_codes"])
        data["promotions_per_day"] = round(data["total_promotions"] / days, 1)
        del data["voucher_codes"]  # Remove set for serialization

    return by_platform

Promotion Value Comparison

def compare_promotion_value(promotions_by_platform):
    """Compare the effective value of promotions across platforms."""
    comparison = {}

    for platform, promos in promotions_by_platform.items():
        active_promos = [p for p in promos if p.is_active]

        if not active_promos:
            continue

        # Calculate effective discount rates
        percentage_promos = [p for p in active_promos if p.discount_type == "percentage"]
        fixed_promos = [p for p in active_promos if p.discount_type == "fixed"]
        free_delivery = [p for p in active_promos if p.discount_type == "free_delivery"]

        comparison[platform] = {
            "total_active": len(active_promos),
            "percentage_discounts": {
                "count": len(percentage_promos),
                "avg_percentage": round(
                    sum(p.discount_value for p in percentage_promos) / len(percentage_promos), 1
                ) if percentage_promos else 0,
                "max_percentage": max(
                    (p.discount_value for p in percentage_promos), default=0
                )
            },
            "fixed_discounts": {
                "count": len(fixed_promos),
                "avg_value": round(
                    sum(p.discount_value for p in fixed_promos) / len(fixed_promos), 2
                ) if fixed_promos else 0,
                "max_value": max(
                    (p.discount_value for p in fixed_promos), default=0
                )
            },
            "free_delivery_offers": len(free_delivery),
            "new_user_exclusives": len([p for p in active_promos if p.is_new_user_only]),
            "avg_min_order": round(
                sum(p.min_order for p in active_promos) / len(active_promos), 2
            )
        }

    return comparison

Seasonal Pattern Detection

def detect_seasonal_patterns(promotion_history):
    """Identify seasonal and recurring promotion patterns."""
    by_month = {}
    by_day_of_week = {i: [] for i in range(7)}
    by_hour = {i: [] for i in range(24)}

    for promo in promotion_history:
        # Monthly patterns
        month_key = promo.discovered_at.strftime("%Y-%m")
        if month_key not in by_month:
            by_month[month_key] = 0
        by_month[month_key] += 1

        # Day of week patterns
        by_day_of_week[promo.discovered_at.weekday()].append(promo)

        # Hour patterns (for flash sales)
        if promo.promo_type == "flash_sale":
            by_hour[promo.discovered_at.hour].append(promo)

    # Identify peak promotion days
    day_names = ["Monday", "Tuesday", "Wednesday", "Thursday",
                 "Friday", "Saturday", "Sunday"]
    peak_days = sorted(
        [(day_names[day], len(promos)) for day, promos in by_day_of_week.items()],
        key=lambda x: x[1],
        reverse=True
    )

    # Identify flash sale peak hours
    peak_hours = sorted(
        [(hour, len(promos)) for hour, promos in by_hour.items() if promos],
        key=lambda x: x[1],
        reverse=True
    )[:5]

    return {
        "monthly_volume": by_month,
        "peak_promotion_days": peak_days[:3],
        "flash_sale_peak_hours": peak_hours,
        "busiest_month": max(by_month.items(), key=lambda x: x[1]) if by_month else None
    }

Monitoring and Alerts

Real-Time Promotion Alerts

class PromotionAlertSystem:
    def __init__(self):
        self.watched_competitors = []
        self.alert_thresholds = {}

    def watch_competitor(self, restaurant_id, platform):
        self.watched_competitors.append({
            "restaurant_id": restaurant_id,
            "platform": platform
        })

    def check_for_alerts(self, new_promotions, previous_promotions):
        """Check for noteworthy promotion changes."""
        alerts = []

        # Detect new high-value promotions
        for promo in new_promotions:
            if promo.fingerprint not in {p.fingerprint for p in previous_promotions}:
                if promo.discount_value >= 30 and promo.discount_type == "percentage":
                    alerts.append({
                        "type": "high_value_promotion",
                        "severity": "high",
                        "message": f"New {promo.discount_value}% discount on {promo.platform} "
                                  f"({promo.country}): {promo.title}",
                        "promotion": promo
                    })

                # Check if competitor launched a promotion
                for watched in self.watched_competitors:
                    if watched["restaurant_id"] in promo.applicable_restaurants:
                        alerts.append({
                            "type": "competitor_promotion",
                            "severity": "medium",
                            "message": f"Competitor launched promotion: {promo.title}",
                            "promotion": promo
                        })

        # Detect expired promotions
        for prev in previous_promotions:
            if prev.fingerprint not in {p.fingerprint for p in new_promotions}:
                if prev.is_active:  # Was active, now gone
                    alerts.append({
                        "type": "promotion_ended",
                        "severity": "low",
                        "message": f"Promotion ended: {prev.title} on {prev.platform}",
                        "promotion": prev
                    })

        return alerts

Scheduling and Automation

Optimal Scanning Schedule

SCAN_SCHEDULE = {
    "platform_vouchers": {
        "frequency": "every_2_hours",
        "rationale": "New vouchers released throughout the day"
    },
    "flash_sales": {
        "frequency": "every_30_minutes",
        "rationale": "Flash sales can appear and expire quickly"
    },
    "restaurant_promotions": {
        "frequency": "every_6_hours",
        "rationale": "Restaurant promos change less frequently"
    },
    "campaign_pages": {
        "frequency": "every_4_hours",
        "rationale": "Campaign pages updated several times daily"
    },
    "payment_promotions": {
        "frequency": "daily",
        "rationale": "Payment partner deals change weekly/monthly"
    }
}

Proxy Strategy for Promotion Tracking

Promotion tracking requires proxies that can:

  1. Access country-specific content: Promotions are geo-targeted, so you need IPs from each target country
  2. Maintain consistency: Some promotions are only visible to returning users, requiring session persistence
  3. Handle high frequency: Flash sale tracking needs frequent requests without triggering rate limits
  4. Switch between platforms: Each scan cycle hits multiple platforms

DataResearchTools mobile proxies excel at promotion tracking because they provide genuine mobile carrier IPs from all major SEA countries, supporting both rotating and sticky session modes needed for comprehensive promotion discovery.

Conclusion

Automated promotion tracking transforms a chaotic landscape of food delivery deals into structured, actionable intelligence. By combining mobile proxy infrastructure from DataResearchTools with systematic scraping and analysis tools, businesses can monitor the full promotional landscape across GrabFood, Foodpanda, ShopeeFood, and GoFood in real time.

The insights gained from promotion tracking inform pricing strategy, marketing timing, and competitive positioning, giving F&B operators in Southeast Asia a significant advantage in one of the world’s most promotion-driven food delivery markets.


Related Reading

Scroll to Top