Live tracking
Runnable examples for querying live aircraft positions and feed statistics. See the full ADS-B Aircraft Tracking reference for filter parameters and response shape.
| # | Question | Data used |
|---|---|---|
| 1 | How many aircraft are airborne right now, and what's the average altitude? | /adsb/aircraft/statistics |
| 2 | Which aircraft are flying in the airspace around KJFK right now? | /adsb/aircraft |
| 3 | How are aircraft distributed by altitude band in a European airspace bbox? | /adsb/aircraft |
Requirements
x-api-key on every request (direct subscription). Learn more →Install
The scripts below use requests for API calls and rich for terminal output. Install once:
pip install requests rich1. Live airspace statistics
Question: How many aircraft are being tracked right now, how many are airborne, and what is the average altitude?
The statistics endpoint aggregates counts across the entire feed — no parameters required.
import os
import requests
from rich.console import Console
from rich.panel import Panel
console = Console()
HEADERS = {
"X-RapidAPI-Key": os.getenv("RAPIDAPI_KEY", "YOUR_RAPIDAPI_KEY"),
"X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
}
BASE = "https://skylink-api.p.rapidapi.com"
def fetch_statistics() -> dict:
r = requests.get(
f"{BASE}/adsb/aircraft/statistics",
headers=HEADERS,
timeout=(10, 15),
)
r.raise_for_status()
return r.json()
def main() -> None:
stats = fetch_statistics()
alt = stats.get("altitude_stats") or {}
avg_ft = alt.get("avg_altitude")
max_ft = alt.get("max_altitude")
avg_fl = round(avg_ft / 100) if avg_ft is not None else None
max_fl = round(max_ft / 100) if max_ft is not None else None
lines = [
f"[bold]Total tracked:[/bold] {stats.get('total_aircraft', 0):,}",
f"[bold]Airborne:[/bold] {stats.get('airborne', 0):,}",
f"[bold]On ground:[/bold] {stats.get('on_ground', 0):,}",
]
if avg_ft is not None:
lines.append(f"[bold]Avg altitude:[/bold] {avg_ft:,.0f} ft (FL{avg_fl:03d})")
if max_ft is not None:
lines.append(f"[bold]Max altitude:[/bold] {max_ft:,.0f} ft (FL{max_fl:03d})")
console.print(Panel("\n".join(lines), title="Live ADS-B Statistics", border_style="cyan"))
console.print(
"[dim]ADS-B data refreshes every few seconds — do not cache these results.[/dim]"
)
if __name__ == "__main__":
main()Output: A panel with total tracked, airborne count, on-ground count, average altitude in ft and FL, and max altitude.
Note: ADS-B data is ephemeral — the feed refreshes every few seconds. Do not cache responses; always fetch fresh data for real-time use cases.
2. Aircraft in the airspace around KJFK
Question: Which aircraft are currently airborne within 80 km of John F. Kennedy International?
Filter with lat, lon, and radius, then exclude on-ground entries for an airborne-only view. Results are sorted by altitude descending.
import os
import requests
from rich.console import Console
from rich.table import Table
console = Console()
HEADERS = {
"X-RapidAPI-Key": os.getenv("RAPIDAPI_KEY", "YOUR_RAPIDAPI_KEY"),
"X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
}
BASE = "https://skylink-api.p.rapidapi.com"
# KJFK coordinates
LAT = 40.6413
LON = -73.7781
RADIUS_KM = 80
LIMIT = 20
def fetch_aircraft_near(lat: float, lon: float, radius: int, limit: int) -> list[dict]:
r = requests.get(
f"{BASE}/adsb/aircraft",
headers=HEADERS,
params={"lat": lat, "lon": lon, "radius": radius, "limit": limit},
timeout=(10, 15),
)
r.raise_for_status()
return r.json().get("aircraft", [])
def main() -> None:
aircraft = fetch_aircraft_near(LAT, LON, RADIUS_KM, LIMIT)
airborne = [
a for a in aircraft
if not a.get("is_on_ground")
]
airborne.sort(key=lambda a: a.get("altitude") or 0, reverse=True)
table = Table(
title=f"Airborne Aircraft within {RADIUS_KM} km of KJFK ({len(airborne)} shown)",
border_style="cyan",
)
table.add_column("Callsign")
table.add_column("ICAO24")
table.add_column("Altitude (ft)", justify="right")
table.add_column("Speed (kt)", justify="right")
table.add_column("Track (°)", justify="right")
table.add_column("Type")
for a in airborne:
alt = f"{int(a['altitude']):,}" if a.get("altitude") is not None else "—"
spd = str(int(a["ground_speed"])) if a.get("ground_speed") is not None else "—"
trk = f"{a['track']:.0f}" if a.get("track") is not None else "—"
table.add_row(
a.get("callsign") or "—",
a.get("icao24") or "—",
alt,
spd,
trk,
a.get("aircraft_type") or "—",
)
console.print(table)
console.print("[dim]On-ground aircraft excluded. ADS-B data refreshes every few seconds.[/dim]")
if __name__ == "__main__":
main()Output: A table of airborne aircraft sorted by altitude descending: Callsign | ICAO24 | Altitude (ft) | Speed (kt) | Track (°) | Type.
3. Altitude band distribution over a European bbox
Question: How are aircraft currently distributed across altitude bands in the Paris–Frankfurt corridor?
Query a bounding box with bbox=lat1,lon1,lat2,lon2, then categorize each aircraft into Ground / Low / Medium / High. Handles None altitudes using the is_on_ground flag.
import os
from collections import defaultdict
import requests
from rich.console import Console
from rich.table import Table
console = Console()
HEADERS = {
"X-RapidAPI-Key": os.getenv("RAPIDAPI_KEY", "YOUR_RAPIDAPI_KEY"),
"X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
}
BASE = "https://skylink-api.p.rapidapi.com"
# Roughly Paris–Frankfurt bounding box
BBOX = "48,2,52,8"
def fetch_bbox(bbox: str) -> list[dict]:
r = requests.get(
f"{BASE}/adsb/aircraft",
headers=HEADERS,
params={"bbox": bbox, "limit": 200},
timeout=(10, 15),
)
r.raise_for_status()
return r.json().get("aircraft", [])
def classify(aircraft: dict) -> str | None:
alt = aircraft.get("altitude")
if alt is None:
return "Ground" if aircraft.get("is_on_ground") else None
if alt < 100:
return "Ground"
if alt < 10_000:
return "Low (<10,000 ft)"
if alt <= 25_000:
return "Medium (10,000–25,000 ft)"
return "High (>25,000 ft)"
def main() -> None:
aircraft = fetch_bbox(BBOX)
bands: dict[str, list[str]] = defaultdict(list)
for a in aircraft:
band = classify(a)
if band is None:
continue
callsign = a.get("callsign") or a.get("icao24") or "?"
bands[band].append(callsign)
order = ["Ground", "Low (<10,000 ft)", "Medium (10,000–25,000 ft)", "High (>25,000 ft)"]
table = Table(title=f"Altitude Distribution — bbox {BBOX}", border_style="cyan")
table.add_column("Band", style="bold")
table.add_column("Count", justify="right")
table.add_column("Example callsigns")
for band in order:
entries = bands.get(band, [])
examples = ", ".join(entries[:3]) if entries else "—"
table.add_row(band, str(len(entries)), examples)
console.print(table)
console.print(
f"[dim]Total aircraft returned: {len(aircraft)}. "
"Aircraft with unknown altitude and not on_ground are excluded.[/dim]"
)
if __name__ == "__main__":
main()Output: A table with four altitude bands, the count in each, and up to three example callsigns per band.