Displays and devices
Desk displays, e-paper frames, LED panels, ESP32 and Raspberry Pi: how to show nearby aircraft without wasting requests, from one unit to a few thousand.
Two patterns, depending on how many units
Each device asks for itself
For a few units. It’s the simplest: the device asks for the aircraft in its radius. But the key sits in the firmware, where it can be extracted, and every extra device counts on the same key.
Your server for all of them
From a few dozen units up. Your server fetches once (your customers’ area, or the whole world on Pro) and each device asks the server only for the aircraft near it. Usage doesn’t grow with the units you sell and the key never leaves the server.
What one device uses
The data refreshes every 2 seconds; for a display 15–30 seconds is usually enough. Requests a month for a device asking for itself:
| Every | Per month, per device | Devices on the Free plan |
|---|---|---|
| 5 s | 518,400 | none |
| 15 s | 172,800 | 1 |
| 30 s | 86,400 | 3 |
| 60 s | 43,200 | 6 |
With the server pattern there is one count only: one request every 30 seconds makes 86,400 requests a month, however many devices you have.
How big a response is
How many aircraft and bytes one request returns, in a few places:
| Area | Radius | Aircraft | JSON | With gzip |
|---|---|---|---|---|
| Milan Malpensa | 15 km | 5 | 4 KB | 1 KB |
| London Heathrow | 50 km | 37 | 33 KB | 7 KB |
| New York JFK | 50 km | 105 | 63 KB | 10 KB |
| Frankfurt | 500 km | 662 | 670 KB | 111 KB |
| Whole world | — | ~11,000 | 7.2 MB | 1.1 MB |
Measured on 26 October 2026, 20:24 UTC. Evening in Europe: there are more aircraft during the day.
The code
curl --compressed -H "X-Api-Key: YOUR_KEY" \
https://api.flyitalyadsb.com/v2/lat/51.47/lon/-0.45/dist/15 // ESP32 (Arduino core 3.x) + ArduinoJson 7
// Aircraft within 15 km of the device, every 30 seconds.
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
const char* WIFI_SSID = "...";
const char* WIFI_PASS = "...";
const char* API_KEY = "YOUR_KEY"; // better: ask your own server, which holds the key
const char* URL = "https://api.flyitalyadsb.com/v2/lat/45.63/lon/8.72/dist/15";
void setup() {
Serial.begin(115200);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) delay(500);
}
void loop() {
WiFiClientSecure tls;
tls.setInsecure(); // demo only: pin the root certificate in a product
HTTPClient http;
http.useHTTP10(true); // no chunked encoding: ArduinoJson reads the stream
http.begin(tls, URL);
http.addHeader("X-Api-Key", API_KEY);
http.setUserAgent("my-desk-display/1.0");
int status = http.GET();
if (status == 200) {
JsonDocument filter; // keep only the fields the display shows
filter["ac"][0]["flight"] = true;
filter["ac"][0]["t"] = true;
filter["ac"][0]["alt_baro"] = true;
JsonDocument doc;
if (!deserializeJson(doc, http.getStream(), DeserializationOption::Filter(filter))) {
for (JsonObject a : doc["ac"].as<JsonArray>()) {
Serial.print(a["flight"] | "?");
Serial.print(" ");
Serial.print(a["t"] | "?");
Serial.print(" ");
if (a["alt_baro"].is<int>()) Serial.println(a["alt_baro"].as<int>());
else Serial.println("ground");
}
}
} else {
Serial.printf("HTTP %d\n", status); // 429: over the limit, retry later
}
http.end();
delay(30000); // every 30 s: 86,400 requests a month
} # server.py: one request for all your devices (Python 3.9+, no dependencies)
import gzip, json, math, os, threading, time, urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs, urlparse
KEY = os.environ["FIA_API_KEY"]
# Pro and Business: the whole world. On Free use your area (up to 500 km).
SOURCE = "https://api.flyitalyadsb.com/v2/lat/0/lon/0/dist/20000"
EVERY_S = 30
snapshot = {"now": 0, "ac": []}
def refresh():
while True:
try:
req = urllib.request.Request(SOURCE, headers={
"X-Api-Key": KEY, "Accept-Encoding": "gzip", "User-Agent": "my-product/1.0"})
with urllib.request.urlopen(req, timeout=30) as r:
body = r.read()
if r.headers.get("Content-Encoding") == "gzip":
body = gzip.decompress(body)
snapshot.update(json.loads(body))
except Exception as e: # keep serving the last copy
print("refresh failed:", e)
time.sleep(EVERY_S)
def km(la1, lo1, la2, lo2):
p = math.pi / 180
h = (math.sin((la2 - la1) * p / 2) ** 2
+ math.cos(la1 * p) * math.cos(la2 * p) * math.sin((lo2 - lo1) * p / 2) ** 2)
return 12742 * math.asin(math.sqrt(h))
class Near(BaseHTTPRequestHandler):
def do_GET(self): # /near?lat=51.47&lon=-0.45&km=15
q = {k: float(v[0]) for k, v in parse_qs(urlparse(self.path).query).items()}
ac = [a for a in snapshot["ac"]
if "lat" in a and km(q["lat"], q["lon"], a["lat"], a["lon"]) <= q.get("km", 15)]
body = json.dumps({"now": snapshot["now"], "ac": ac}).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
threading.Thread(target=refresh, daemon=True).start()
ThreadingHTTPServer(("0.0.0.0", 8080), Near).serve_forever() Replace YOUR_KEY with your key: it’s free, get it here. Full specification in openapi.json.
Watch out for
- A key in firmware can be extracted. If you sell many units, keep it on your server.
- A small radius on an ESP32. A 15 km response is a few KB; 50 km over New York is already 63 KB. Filter the fields as in the example, or let your server do it.
- A User-Agent of your own on every request, and the
X-Api-Keyheader. - On a 429, wait the
Retry-Afterseconds instead of retrying straight away: refused requests don’t count, but repeating them doesn’t help. - Attribution on the device or in the manual and on the product page: the line to use.
- The feeder perk (Pro limits for free) is for non-commercial use: it doesn’t cover units you sell.