Turning Waze Into an OSINT Tool: A Python Tutorial

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.