If you have ever wanted to build your own version of Flightradar24 — a map that shows live aircraft moving in real time — this guide is for you. By the end you will have a working Python flight tracker that pulls real-time ADS-B positions, filters them to a geographic area, and renders them on an interactive map that auto-refreshes every few seconds.

We will use the SkyLink ADS-B API, which exposes raw receiver data — icao24 hex address, callsign, latitude, longitude, altitude, ground speed, heading, registration, aircraft type and airline — through a single REST endpoint. No SDR hardware, no antenna on your roof, no decoding 1090 MHz yourself.

What you will build:

  • A Python client that authenticates against the SkyLink API
  • A bounding-box query that returns every aircraft over a region
  • A polling loop that refreshes positions every ~5 seconds
  • A live, auto-updating Folium map
  • Production-grade handling for rate limits, retries and caching

Estimated time: 20 minutes. Full source is at the bottom.

Prerequisites

  • Python 3.9 or newer
  • A SkyLink API key — get one through the free trial (1,000 requests/month)
  • Basic familiarity with HTTP requests

Install the two libraries we need:

pip install requests folium

Step 1 — Authenticate

The examples below use the RapidAPI gateway, so every request carries two headers: your key and the host. If you bought direct, send a single x-api-key header against data.skylinkapi.com instead. Never hard-code your key in source you plan to share — read it from an environment variable instead.

import os
import requests

BASE_URL = "https://skylink-api.p.rapidapi.com"
HEADERS = {
    "X-RapidAPI-Key": os.environ["SKYLINK_API_KEY"],
    "X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
}

def get(path, params=None):
    r = requests.get(f"{BASE_URL}{path}", headers=HEADERS, params=params, timeout=10)
    r.raise_for_status()
    return r.json()

Set your key once in the terminal:

export SKYLINK_API_KEY="your_rapidapi_key_here"   # Windows: setx SKYLINK_API_KEY "..."

Step 2 — Query aircraft in a bounding box

The /adsb/aircraft endpoint accepts a bbox parameter — a bounding box written as lat1,lon1,lat2,lon2 with the south-west corner first. The example below grabs everything currently flying over the New York metro area.

# SW corner (lat, lon), NE corner (lat, lon)
NYC_BBOX = "40.4,-74.3,41.0,-73.5"

def aircraft_in_box(bbox):
    data = get("/adsb/aircraft", params={"bbox": bbox})
    return data.get("aircraft", data if isinstance(data, list) else [])

for ac in aircraft_in_box(NYC_BBOX)[:5]:
    print(ac["callsign"], ac["altitude_baro"], "ft", ac["velocity"], "kt")

A single record looks like this:

{
  "icao24": "a1b2c3",
  "callsign": "UAL245",
  "lat": 40.71,
  "lon": -74.01,
  "altitude_baro": 31000,
  "velocity": 447,
  "heading": 268,
  "registration": "N12345",
  "aircraft_type": "B738",
  "airline": "United Airlines"
}

You can narrow results further with altitude, speed, callsign, registration or airline filters, or use radius (in km) around a point instead of a bounding box. For example, only airborne jets above 10,000 ft:

get("/adsb/aircraft", params={"bbox": NYC_BBOX, "altitude": "10000-45000"})

Step 3 — Poll for live updates

ADS-B positions on SkyLink refresh roughly every 5–10 seconds, so polling faster than that wastes quota without giving you newer data. A simple loop with a sane interval is all you need.

import time

def stream(bbox, interval=6):
    while True:
        try:
            fleet = aircraft_in_box(bbox)
            print(f"{len(fleet)} aircraft in view")
            yield fleet
        except requests.HTTPError as e:
            # 429 = rate limited; back off and retry
            wait = 30 if e.response is not None and e.response.status_code == 429 else interval
            print(f"error {e}; backing off {wait}s")
            time.sleep(wait)
            continue
        time.sleep(interval)

Step 4 — Render a live map

Now turn those coordinates into something visual. We rotate each marker to match the aircraft's heading and label it with the callsign and altitude.

import folium

def render(fleet, center=(40.7, -73.9)):
    m = folium.Map(location=center, zoom_start=9, tiles="cartodbpositron")
    for ac in fleet:
        if ac.get("lat") is None or ac.get("lon") is None:
            continue
        folium.Marker(
            location=(ac["lat"], ac["lon"]),
            tooltip=f'{ac.get("callsign","?")} · {ac.get("altitude_baro","?")} ft',
            icon=folium.DivIcon(html=(
                f'<div style="transform: rotate({ac.get("heading",0)}deg);'
                f'font-size:18px">&#9650;</div>'
            )),
        ).add_to(m)
    m.save("flights.html")
    return m

# One-shot snapshot
render(aircraft_in_box(NYC_BBOX))

Open flights.html in a browser and you will see live traffic. To make it self-refresh, either add an HTML meta-refresh tag to the saved file or wrap the whole thing in a small Flask or Streamlit app that re-renders on an interval.

Step 5 — Production tips

A weekend project and a production service are different animals. Three things matter once real users depend on your tracker:

  • Respect rate limits. Each plan has a monthly request quota. Cache the last response and serve it to all connected clients rather than calling the API once per user — one poll every 6 seconds is ~430k calls/month, so size your plan or cache accordingly.
  • Handle gaps. Aircraft drop in and out of coverage. Keep a short in-memory history keyed by icao24 so a momentarily missing aircraft does not flicker off your map.
  • Enrich once, not every frame. Registration, type and airline are stable for a given icao24. Cache them so you are not re-fetching static metadata on every poll.

Full source

import os, time, requests, folium

BASE_URL = "https://skylink-api.p.rapidapi.com"
HEADERS = {
    "X-RapidAPI-Key": os.environ["SKYLINK_API_KEY"],
    "X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
}
NYC_BBOX = "40.4,-74.3,41.0,-73.5"

def aircraft_in_box(bbox):
    r = requests.get(f"{BASE_URL}/adsb/aircraft", headers=HEADERS,
                     params={"bbox": bbox}, timeout=10)
    r.raise_for_status()
    data = r.json()
    return data.get("aircraft", data if isinstance(data, list) else [])

def render(fleet, center=(40.7, -73.9)):
    m = folium.Map(location=center, zoom_start=9, tiles="cartodbpositron")
    for ac in fleet:
        if ac.get("lat") is None or ac.get("lon") is None:
            continue
        folium.Marker(
            (ac["lat"], ac["lon"]),
            tooltip=f'{ac.get("callsign","?")} · {ac.get("altitude_baro","?")} ft',
            icon=folium.DivIcon(html=f'<div style="transform:rotate({ac.get("heading",0)}deg);font-size:18px">&#9650;</div>'),
        ).add_to(m)
    m.save("flights.html")

if __name__ == "__main__":
    while True:
        try:
            render(aircraft_in_box(NYC_BBOX))
            print("map updated")
        except requests.HTTPError as e:
            print("error", e)
            time.sleep(30)
        time.sleep(6)

Where to go next

  • Add weather context with the METAR/TAF endpoints so your map shows airport conditions
  • Pull flight schedules and status to label arrivals and departures
  • Swap Folium for Leaflet or Mapbox GL on the frontend and stream positions over WebSockets
  • Building an AI assistant instead? SkyLink ships an MCP server so models like Claude can query live aviation data directly

Everything here runs on one API key. Grab a free trial key and start tracking.