API Documentation
Live VDL2 datalink — what aircraft are saying (engine/ACMS reports, OOOI times, weather, intent) — over REST and WebSocket.
Authentication
Every feeder gets a unique API key. Use it from any IP โ your laptop, VPS, phone, scripts.
curl -H "Authorization: Bearer YOUR_API_KEY" https://adsbiq.com/api/v2/all
Your API key is shown on the join page and your feeder dashboard after install.
Alternative: IP-based auth (no key needed from home network)
Requests from your feeder's IP are automatically authenticated โ no header needed:
curl https://adsbiq.com/api/v2/all
Not a feeder? Install in 60 seconds to get access.
Access & Rate Limits
All feeds (REST, WebSocket) are exclusively for active feeders. Non-feeders get 403. Become a feeder to get access.
| Feed | Limit |
|---|---|
| REST API | 60 req burst / 10 per minute sustained. Exceeding returns 429. |
| WebSocket | Server-push โ no request rate limit. One connection per feeder. |
๐ฐ๏ธ VDL2 — Datalink Messages (REST) our priority
What aircraft are saying โ engine/ACMS reports, OOOI, weather, intent. A different shape from position data: timestamped text messages, not lat/lon.
feed.adsbiq.com:5552 and the read
endpoints below serve a hot recent-message window (newest first). Same auth and rate limits as the
aircraft API. Want to feed VDL2? See /vdl2.
Endpoints
/api/v2/messages/recent
The live tail across all aircraft, newest first. Query: ?limit={1-1000},
?since={unix|ISO-8601}.
/api/v2/messages/tail/{reg}
Recent datalink messages for an aircraft registration (e.g. N827NN).
/api/v2/messages/hex/{icao}
Recent messages by ICAO hex (e.g. a12345).
/api/v2/messages/label/{label}
Messages by ACARS label (e.g. H1 engine, 5Z OOOI). Query:
?since=, ?until=, ?limit=.
/api/v2/messages/stats
Live volume snapshot: global and per-feeder messages/min, active VDL2 feeder count.
Deep history. The endpoints above serve a hot recent window. Add
?history=1 (with ?since=/?until=) to scan the archived
message store instead โ bounded, newest-first, with "mode":"archive" and a
"truncated" flag when the scan hits its cap.
Example
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://adsbiq.com/api/v2/messages/recent?limit=20"
# -> { "messages": [ โฆ ], "count": 20, "source": "vdl2" }
Message schema
Each record is the canonical, lossless form written at ingest:
{
"uuid": "โฆ", // globally unique message id
"timestamp": 1717761600.25, // unix seconds (UTC)
"sourceType": "VDL2",
"icaoHex": "ac2bb1", // aircraft ICAO address
"tail": "N827NN", // registration
"flightNumber": "AAL1234",
"label": "H1", // ACARS label
"blockId": "4", "msgNum": "M62A", "ack": "!",
"text": "ENG RPT N1 95.2 EGT 612",
"frequency": 136975000, // Hz
"level": -19.7, // signal level (dBFS)
"contentHash": "โฆ" // receiver-independent dedup key
}
// Receiving-feeder identity (IP / station id) is never exposed โ operator privacy.
Backed by the S3 + local-NVMe parquet archive (no Postgres). Coverage and history queries read the canonical parquet; the endpoints above will serve recent windows with low latency.
WebSocket Feed
Real-time push-based aircraft data โ no polling needed
Connection
| Endpoint | wss://adsbiq.com/ws/ |
| Auth | IP-based (same as REST API) or Bearer token via query: wss://adsbiq.com/ws/?token=YOUR_API_KEY |
| Compression | permessage-deflate (negotiated automatically) |
| Ping/Pong | Server sends ping every 30s, timeout 10s |
| Max message size | 512 bytes (client โ server) |
Channels
The WebSocket serves three channels on the same endpoint — the VDL2 datalink stream (our priority) plus two ADS-B position channels. Choose based on your use case:
| Channel | Interval | Coverage | How to connect |
|---|---|---|---|
| ๐ฐ๏ธ VDL2 PRIORITY | Real-time (pushed as decoded) | Live VDL2/ACARS datalink messages from aircraft | wss://adsbiq.com/ws/?channel=vdl2 |
| ๐ก Global (ADS-B, default) | Every 10 seconds | All aircraft worldwide | wss://adsbiq.com/ws/ |
| ๐ก Zone (ADS-B) | Every 1 second | Aircraft within 250 nm of your location | wss://adsbiq.com/ws/?lat=40.7&lon=-74.0 |
permessage-deflate) stream of
decoded aircraft datalink messages โ engine/ACMS reports, OOOI, weather โ pushed the moment they're decoded.
First frame is {"type":"vdl2_snapshot", "messages":[โฆ]}
(recent, oldest-first); thereafter {"type":"vdl2", "messages":[โฆ]} as new messages arrive.
Each message is the canonical record (tail, flightNumber, label, text, icaoHex, frequency, level, timestamp);
receiver identity (IP/station) is never included. Same feeder auth as the other channels.
# Python: subscribe to the live VDL2 stream
import asyncio, json, websockets
async def main():
async with websockets.connect("wss://adsbiq.com/ws/?channel=vdl2") as ws:
async for frame in ws:
for m in json.loads(frame).get("messages", []):
print(m["timestamp"], m.get("tail"), m.get("label"), m.get("text"))
asyncio.run(main())
Breaking change: The default (global) channel now updates every 10s instead of 1s.
If you need 1-second updates, switch to zone mode by adding ?lat=X&lon=Y to your connection URL.
Client Messages
Clients can switch channels mid-session by sending JSON messages:
| Action | Payload | Effect |
|---|---|---|
set_zone |
{"action": "set_zone", "lat": 40.7, "lon": -74.0} |
Switch to zone channel centered on (lat, lon). Triggers immediate full snapshot. |
set_global |
{"action": "set_global"} |
Switch back to global channel. Triggers full snapshot on next 10s tick. |
Message Types
The first message after connect is a full snapshot. Subsequent messages are deltas containing only changes. Every message includes a channel field ("global" or "zone").
Full Snapshot
{
"type": "full",
"channel": "zone",
"seq": 42,
"now": 1774187176.0,
"messages": 6505606,
"total": 51,
"ctime": 1774187176.3,
"ptime": 1774187175.9,
"ac": [ ... ]
}
Delta Update
{
"type": "delta",
"channel": "zone",
"seq": 43,
"now": 1774187177.0,
"messages": 6505620,
"total": 52,
"ctime": 1774187177.3,
"ptime": 1774187176.9,
"new": [
{"hex": "c99aab", "flight": "SWA789 ", "lat": 42.0, "lon": -72.0, "alt_baro": 30000, ...}
],
"update": [
{"hex": "ad5d5c", "lat": 26.3, "alt_baro": 34000}
],
"remove": ["a1b2c3"]
}
Delta Fields
| Field | Description |
|---|---|
type | "full" or "delta" |
channel | "global" or "zone" |
seq | Monotonic sequence number (increments by 1 per message, per channel). If your client sees a gap, reconnect to get a fresh full snapshot. |
total | Current aircraft count (zone = nearby count, global = worldwide). Clients SHOULD verify Object.keys(aircraft).length === msg.total after applying each delta โ mismatch means state drift, reconnect. |
new | Full aircraft objects appearing for the first time (omitted if empty) |
update | Sparse objects: hex + only the fields that changed (omitted if empty) |
remove | Array of hex codes no longer tracked (omitted if empty) |
Client-Side Merge Pattern
update objects are sparse โ they contain only hex plus fields that actually differ from the previous message. Merge them into your local state, do not replace:
// JavaScript โ global channel (10s updates, all aircraft)
const ws = new WebSocket("wss://adsbiq.com/ws/");
// JavaScript โ zone channel (1s updates, 250nm radius)
// const ws = new WebSocket("wss://adsbiq.com/ws/?lat=40.7&lon=-74.0");
const aircraft = {};
let lastSeq = 0;
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
// Sequence gap detection โ reconnect if we missed a frame
if (msg.type === "delta" && msg.seq !== lastSeq + 1) {
ws.close(); // reconnect logic will get a fresh full
return;
}
lastSeq = msg.seq;
if (msg.type === "full") {
for (const k in aircraft) delete aircraft[k];
for (const ac of msg.ac) aircraft[ac.hex] = ac;
} else {
for (const ac of msg.new || []) aircraft[ac.hex] = ac;
for (const ac of msg.update || []) Object.assign(aircraft[ac.hex], ac);
for (const hex of msg.remove || []) delete aircraft[hex];
}
// Consistency check โ total must match local state
if (Object.keys(aircraft).length !== msg.total) {
ws.close(); // state drift โ reconnect for fresh full
}
};
// Switch to zone mid-session:
// ws.send(JSON.stringify({action: "set_zone", lat: 40.7, lon: -74.0}));
// Switch back to global:
// ws.send(JSON.stringify({action: "set_global"}));
Trigger Fields
Deltas are triggered by changes to: lat, lon, alt_baro, alt_geom, gs, ias, tas, mach, track, track_rate, roll, mag_heading, true_heading, baro_rate, geom_rate, squawk, emergency, flight, nav_qnh, nav_altitude_mcp, nav_altitude_fms, nav_heading, category, r, t, route_origin, route_dest.
Volatile fields (rssi, seen, seen_pos, messages) alone do not trigger an update, but are included in the sparse object if they changed alongside a trigger field.
Example Code
Get started quickly with a working Flask demo that queries every endpoint:
git clone https://github.com/adsbiq/adsbiq-api-demo.git
cd adsbiq-api-demo
pip install -r requirements.txt
python app.py
View on GitHub โ includes REST and WebSocket examples.