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.

#QuestionData used
1How many aircraft are airborne right now, and what's the average altitude?/adsb/aircraft/statistics
2Which aircraft are flying in the airspace around KJFK right now?/adsb/aircraft
3How are aircraft distributed by altitude band in a European airspace bbox?/adsb/aircraft

Requirements

Auth
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 rich

1. 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.