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.

FeedLimit
REST API60 req burst / 10 per minute sustained. Exceeding returns 429.
WebSocketServer-push โ€” no request rate limit. One connection per feeder.
๐Ÿ“ก Looking for the ADS-B API? It lives on our sister site โ€” full docs at ADSBiq (adsbiq.com/api/docs) ↗
Status: live. VDL2 ingest runs on 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

GET
/api/v2/messages/recent

The live tail across all aircraft, newest first. Query: ?limit={1-1000}, ?since={unix|ISO-8601}.

GET
/api/v2/messages/tail/{reg}

Recent datalink messages for an aircraft registration (e.g. N827NN).

GET
/api/v2/messages/hex/{icao}

Recent messages by ICAO hex (e.g. a12345).

GET
/api/v2/messages/label/{label}

Messages by ACARS label (e.g. H1 engine, 5Z OOOI). Query: ?since=, ?until=, ?limit=.

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

Endpointwss://adsbiq.com/ws/
AuthIP-based (same as REST API) or Bearer token via query: wss://adsbiq.com/ws/?token=YOUR_API_KEY
Compressionpermessage-deflate (negotiated automatically)
Ping/PongServer sends ping every 30s, timeout 10s
Max message size512 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:

ChannelIntervalCoverageHow 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
The VDL2 channel is unique: a live, compressed (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:

ActionPayloadEffect
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

FieldDescription
type"full" or "delta"
channel"global" or "zone"
seqMonotonic sequence number (increments by 1 per message, per channel). If your client sees a gap, reconnect to get a fresh full snapshot.
totalCurrent 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.
newFull aircraft objects appearing for the first time (omitted if empty)
updateSparse objects: hex + only the fields that changed (omitted if empty)
removeArray 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.