A few days ago, a fascinating article started making the rounds on X (Twitter) by security researcher
@Harrris0n. The premise was simple but striking:
Waze’s public live map exposes enough data to track individuals across 60+ countries.
Every time someone taps that “report police” or “report hazard” button, they’re broadcasting their exact GPS coordinates, a precise timestamp, and potentially identifiable information to anyone who knows where to look.
I wanted to break this down for my OSINT students with working Python code you can actually run today. Let’s dig in.
The Original Research
Full credit goes to
@Harrris0n for the original research and article. The key insight was this:
“It’s one thing to say ‘Waze could theoretically be used for surveillance.’ It’s another to actually build the system, collect the data, and show what a global movement-tracking database looks like.”
The researcher built a system that:
- Segments cities into hyperlocal geographic grids based on population density
- Queries each grid cell every 3-5 minutes
- Captures approximately 95% of all new alerts across major cities in 60 countries
The result? A database of user movements that can reveal where people live, where they work, and what routes they take.
What Data Does Waze Expose?
When you submit a report on Waze, the following becomes publicly accessible:
| Data Point |
OSINT Value |
| GPS Coordinates |
Exact location (latitude/longitude) |
| Timestamp |
Precise time (to the millisecond) |
| Report Type |
What they reported (police, hazard, etc.) |
| City/Street |
Location context |
| Thumbs Up Count |
How many confirmed the report |
String enough of these data points together for a single area, and patterns emerge. Morning clusters might indicate home locations. Daytime clusters suggest workplaces. The routes between them? Daily commutes.
The Working API Endpoint
After some digging, I found the working API endpoint that Waze’s live map uses:
https://www.waze.com/live-map/api/georss
The critical parameters are:
| Parameter |
Description |
top |
Northern latitude boundary |
bottom |
Southern latitude boundary |
left |
Western longitude boundary |
right |
Eastern longitude boundary |
env |
Region code (critical!) |
types |
Data types to fetch |
The
env parameter is the key that makes this work:
na = North America
row = Rest of World (Europe, Asia, Africa, etc.)
il = Israel
Without the correct
env parameter, the API returns errors. With it, you get JSON data for any location on Earth where Waze operates.
Let’s Build It: The Python Code
Here’s a clean, educational implementation. I’ve kept it simple and well-documented so you can understand exactly what’s happening.
Step 1: Setup and Configuration
import requests
import json
from datetime import datetime
from dataclasses import dataclass
from typing import List, Optional
# The working API endpoint
WAZE_API_URL = "https://www.waze.com/live-map/api/georss"
# Headers to mimic a browser request
HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Accept": "application/json, text/plain, */*",
"Referer": "https://www.waze.com/live-map/directions",
}
Nothing fancy here – just the endpoint URL and headers that make the API think we’re a normal browser visiting the live map.
Step 2: Define Our Data Structures
@dataclass
class BoundingBox:
"""Geographic bounding box for queries."""
south: float # bottom latitude
north: float # top latitude
west: float # left longitude
east: float # right longitude
@classmethod
def from_center(cls, lat: float, lon: float, delta: float = 0.05):
"""Create a box from a center point. Delta of 0.05 = ~5km."""
return cls(
south=lat - delta,
north=lat + delta,
west=lon - delta,
east=lon + delta,
)
The
BoundingBox class makes it easy to define geographic areas. The
from_center method lets you specify a point (like a city center) and automatically create a box around it.
@dataclass
class WazeAlert:
"""Represents a single Waze alert/report."""
alert_type: str
subtype: Optional[str]
latitude: float
longitude: float
timestamp: datetime
city: Optional[str]
street: Optional[str]
country: str
reliability: int
uuid: str
thumbs_up: int = 0
Each alert from the API gets parsed into this clean structure. The
uuid field is important for deduplication when scanning large areas.
Step 3: Query the API
def query_waze(bbox: BoundingBox, region: str = "na") -> dict:
"""
Query the Waze live map API.
Args:
bbox: Geographic area to search
region: 'na' (North America), 'row' (Rest of World), or 'il' (Israel)
Returns:
Raw JSON response from the API
"""
params = {
"top": bbox.north,
"bottom": bbox.south,
"left": bbox.west,
"right": bbox.east,
"env": region, # THIS IS THE KEY PARAMETER
"types": "alerts,traffic,users",
}
response = requests.get(WAZE_API_URL, params=params, headers=HEADERS, timeout=30)
response.raise_for_status()
return response.json()
This is the core function. Note the
env parameter – this tells the API which regional server to query. Get this wrong and you’ll get errors.
Step 4: Parse the Response
def parse_alerts(data: dict) -> List[WazeAlert]:
"""Parse raw API response into WazeAlert objects."""
alerts = []
for alert in data.get("alerts", []):
# Location is stored as x (longitude) and y (latitude)
location = alert.get("location", {})
# Timestamp is in milliseconds since epoch
pub_millis = alert.get("pubMillis", 0)
timestamp = datetime.fromtimestamp(pub_millis / 1000) if pub_millis else None
parsed = WazeAlert(
alert_type=alert.get("type", "UNKNOWN"),
subtype=alert.get("subtype"),
latitude=location.get("y", 0),
longitude=location.get("x", 0),
timestamp=timestamp,
city=alert.get("city"),
street=alert.get("street"),
country=alert.get("country", ""),
reliability=alert.get("reliability", 0),
uuid=alert.get("uuid", ""),
thumbs_up=alert.get("nThumbsUp", 0),
)
alerts.append(parsed)
return alerts
The API returns coordinates as
x (longitude) and
y (latitude) – the opposite of what you might expect. Timestamps are in milliseconds, so we divide by 1000.
Step 5: Put It Together
# Query New York City
bbox = BoundingBox.from_center(40.7128, -74.006, delta=0.05)
data = query_waze(bbox, region="na")
alerts = parse_alerts(data)
print(f"Found {len(alerts)} alerts in NYC")
# Show police reports
for alert in alerts:
if "POLICE" in alert.alert_type:
print(f" {alert.street}, {alert.city}")
print(f" Type: {alert.subtype or alert.alert_type}")
print(f" Time: {alert.timestamp}")
print()
That’s it. Run this and you’ll get real-time Waze data for any city.
Handling the 200 Alert Cap
Waze limits responses to about 200 alerts per query. For larger areas, you need to divide and conquer:
def create_grid(bbox: BoundingBox, rows: int, cols: int) -> List[BoundingBox]:
"""Divide a bounding box into smaller cells."""
lat_step = (bbox.north - bbox.south) / rows
lon_step = (bbox.east - bbox.west) / cols
cells = []
for row in range(rows):
for col in range(cols):
cell = BoundingBox(
south=bbox.south + (row * lat_step),
north=bbox.south + ((row + 1) * lat_step),
west=bbox.west + (col * lon_step),
east=bbox.west + ((col + 1) * lon_step),
)
cells.append(cell)
return cells
# Scan LA with a 3x3 grid (9 queries)
la_bbox = BoundingBox(south=33.9, north=34.2, west=-118.5, east=-118.1)
cells = create_grid(la_bbox, 3, 3)
all_alerts = []
seen_uuids = set()
for cell in cells:
data = query_waze(cell, region="na")
for alert in parse_alerts(data):
if alert.uuid not in seen_uuids:
seen_uuids.add(alert.uuid)
all_alerts.append(alert)
time.sleep(1) # Be nice to the servers
print(f"Total unique alerts: {len(all_alerts)}")
The
seen_uuids set prevents counting the same alert twice when it appears in overlapping grid cells.
Practical Applications
Finding Police Report Hotspots
from collections import defaultdict
def find_hotspots(alerts, precision=3):
"""Group alerts by approximate location."""
clusters = defaultdict(list)
for alert in alerts:
# Round coordinates to cluster nearby points
key = f"{round(alert.latitude, precision)},{round(alert.longitude, precision)}"
clusters[key].append(alert)
# Sort by count
return sorted(clusters.items(), key=lambda x: len(x[1]), reverse=True)
police_alerts = [a for a in alerts if "POLICE" in a.alert_type]
hotspots = find_hotspots(police_alerts)
print("Top police report locations:")
for location, alerts in hotspots[:10]:
print(f" {location}: {len(alerts)} reports")
Export to Google Earth (KML)
def export_to_kml(alerts, filename):
"""Create a KML file for visualization in Google Earth."""
kml = ['<?xml version="1.0" encoding="UTF-8"?>',
'<kml xmlns="http://www.opengis.net/kml/2.2">',
'<Document><name>Waze Alerts</name>']
for alert in alerts:
kml.append(f'''<Placemark>
<name>{alert.alert_type}</name>
<description>{alert.street}, {alert.city}</description>
<Point><coordinates>{alert.longitude},{alert.latitude},0</coordinates></Point>
</Placemark>''')
kml.extend(['</Document>', '</kml>'])
with open(filename, 'w') as f:
f.write('\n'.join(kml))
export_to_kml(alerts, "waze_alerts.kml")
Open the resulting file in Google Earth and you’ll see every alert plotted on the map.
A Note on Privacy
The original researcher made an important point:
“My hope is that this demonstration prompts Waze (and Google, who owns them) to reconsider the privacy tradeoffs in their design.”
It’s worth noting that Waze appears to have already made some changes. The
reportBy field (which contained usernames) is no longer present in the public API response I tested. This suggests they’ve taken steps to anonymize the data.
However, the location and timing data is still there. For someone who reports from the same locations regularly, patterns could still emerge.
Get the Complete Code
I’ve packaged everything into a clean, well-documented Python module. You can grab it from
my GitHub or copy the code above.
Files included:
waze_osint.py – The complete tool
README.md – Documentation
requirements.txt – Just needs requests
Run the demo with:
pip install requests
python waze_osint.py
Final Thoughts
This is a perfect example of why OSINT matters. Data that seems harmless in isolation – “someone reported a pothole” – becomes powerful when aggregated and analyzed.
For my students: this is why we study these techniques. Not to misuse them, but to understand the privacy implications of the tools we use every day. Every app that asks for location access, every service that tracks your movements – they all create data that someone, somewhere, might be able to access.
Stay curious. Stay ethical.
Have questions? Drop them in the comments or reach out on social media.
Full credit to @Harrris0n for the original research that inspired this tutorial.