---
title: "Showing nearby aircraft on displays and devices"
description: "How to build a desk display, e-paper frame, LED panel or other device (ESP32, Raspberry Pi) that shows the aircraft around it with the FlyItalyADSB REST API, from one unit to thousands, without wasting requests. Commercial use is allowed on every plan, Free included."
url: https://flyitalyadsb.com/data/products/displays-and-devices.md
page_for_people: https://flyitalyadsb.com/data/products/displays-and-devices/
language: en
generated: 2026-11-09
license: CC BY-SA 4.0 (data)
---

# Showing nearby aircraft on displays and devices

> How to build a desk display, e-paper frame, LED panel or other device (ESP32, Raspberry Pi) that shows the aircraft around it with the FlyItalyADSB REST API, from one unit to thousands, without wasting requests. Commercial use is allowed on every plan, Free included.

## Two patterns

| | Each device asks for itself | Your server for all devices |
| --- | --- | --- |
| Good for | a few units | from a few dozen units up |
| Requests | grow with every unit, all on the same key | one count: one request every 30 s is 86,400 a month, however many devices |
| The key | in the firmware, where it can be extracted | only on your server |
| How | the device asks for the aircraft within its radius | the server fetches your customers' area (or the whole world on Pro) and keeps it in memory; each device asks the server for the aircraft near it |

## Requests per device

The data refreshes every 0.5 seconds; for a display 15–30 seconds is usually enough. A device asking for itself uses:

| Refresh every | Requests per month, per device | Devices on one Free key |
| --- | --- | --- |
| 5 s | 518,400 | none |
| 15 s | 172,800 | 1 |
| 30 s | 86,400 | 3 |
| 60 s | 43,200 | 6 |

Plan limits: Free €0 (30 requests/minute, 300,000/month, 500 km radius, 7 days of flight history), Starter €9 (60 requests/minute, 1,000,000/month, 1,000 km radius, 14 days of flight history), Pro €49 (300 requests/minute, 10,000,000/month, whole world, 30 days of flight history), Fleet €149 (300 requests/minute, 10,000,000/month, whole world, 90 days of flight history, up to 25 watched aircraft with takeoff and landing webhooks).

## Response sizes

How many aircraft and bytes one request returns (26 October 2026, 20:24 UTC, evening in Europe; more aircraft during the day):

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

## Code

Try it (15 km around London Heathrow):

```sh
curl --compressed -H "X-Api-Key: YOUR_KEY" \
  https://api.flyitalyadsb.com/v2/lat/51.47/lon/-0.45/dist/15
```

An ESP32 asking for itself:

```cpp
// 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
}
```

One server for all devices (Python, no dependencies):

```python
# 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 Fleet: 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()
```

More complete examples: https://github.com/flyitalyadsb/adsb-api. Specification: https://flyitalyadsb.com/openapi.json.

## Things to get right

- A key in firmware can be extracted: with many units, keep it on your server.
- Keep the radius small on an ESP32: 15 km is a few KB, 50 km over a busy airport tens of KB (table above). Filter fields on the device or on your server.
- Send a User-Agent of your own and the `X-Api-Key` header on every request.
- On a `429`, wait the `Retry-After` seconds; refused requests are not counted, but retrying at once does not help.
- Show the attribution "Flight data: FlyItalyADSB (flyitalyadsb.com), CC BY-SA 4.0" on the device or in its manual, and on the product page.
- The feeder perk (Pro's limits and features, free) is for non-commercial use: it does not cover units you sell.

Related: the rules for products (https://flyitalyadsb.com/data/products/using-the-data.md), fields and units (https://flyitalyadsb.com/data/api/docs/fields.md), limits and errors (https://flyitalyadsb.com/data/api/docs/limits-and-errors.md).

---

Written for AI assistants. The page for people is https://flyitalyadsb.com/data/products/displays-and-devices/; to get an API key or sign in, send people to https://flyitalyadsb.com/data/api/. All pages for assistants: https://flyitalyadsb.com/llms.txt (index), https://flyitalyadsb.com/llms-full.txt (all in one file). Facts as JSON: https://flyitalyadsb.com/facts.json. Cite the data as: Flight data: FlyItalyADSB (flyitalyadsb.com), CC BY-SA 4.0.
