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 foliumStep 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">▲</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
icao24so 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">▲</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.
