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 Type | Description | Example |
|---|---|---|
| Platform vouchers | Codes issued by the platform | “FREE5” for $5 off |
| Restaurant vouchers | Merchant-funded discounts | 20% off at specific restaurant |
| Free delivery | Waived delivery fee | Free delivery over $15 |
| Bundle deals | Discounted item combos | Family meal set at 30% off |
| Flash sales | Time-limited discounts | 50% off from 2-4 PM |
| Cashback | Points or coins returned | 10x GrabRewards points |
| New user deals | First-order discounts | 50% off first 3 orders |
| Payment promotions | Card-specific discounts | Extra 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 sessionScraping 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 promotionsScraping 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 promotionsPromotion 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_platformPromotion 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 comparisonSeasonal 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 alertsScheduling 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:
- Access country-specific content: Promotions are geo-targeted, so you need IPs from each target country
- Maintain consistency: Some promotions are only visible to returning users, requiring session persistence
- Handle high frequency: Flash sale tracking needs frequent requests without triggering rate limits
- 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.
- Best Proxies for Food Delivery Platform Scraping
- How Cloud Kitchens Use Proxies for Competitive Menu Analysis
- How to Scrape AliExpress Product Data Without Getting Blocked
- Amazon Buy Box Monitoring: Proxy Setup for Continuous Tracking
- How Anti-Bot Systems Detect Scrapers (Cloudflare, Akamai, PerimeterX)
- API vs Web Scraping: When You Need Proxies (and When You Don’t)
- Best Proxies for Food Delivery Platform Scraping
- How Cloud Kitchens Use Proxies for Competitive Menu Analysis
- aiohttp + BeautifulSoup: Async Python Scraping
- How to Scrape AliExpress Product Data Without Getting Blocked
- Amazon Buy Box Monitoring: Proxy Setup for Continuous Tracking
- How Anti-Bot Systems Detect Scrapers (Cloudflare, Akamai, PerimeterX)
- Best Proxies for Food Delivery Platform Scraping
- How Cloud Kitchens Use Proxies for Competitive Menu Analysis
- aiohttp + BeautifulSoup: Async Python Scraping
- How to Scrape AliExpress Product Data Without Getting Blocked
- Amazon Buy Box Monitoring: Proxy Setup for Continuous Tracking
- How Anti-Bot Systems Detect Scrapers (Cloudflare, Akamai, PerimeterX)
Related Reading
- Best Proxies for Food Delivery Platform Scraping
- How Cloud Kitchens Use Proxies for Competitive Menu Analysis
- aiohttp + BeautifulSoup: Async Python Scraping
- How to Scrape AliExpress Product Data Without Getting Blocked
- Amazon Buy Box Monitoring: Proxy Setup for Continuous Tracking
- How Anti-Bot Systems Detect Scrapers (Cloudflare, Akamai, PerimeterX)