NOREVYN / API Reference

NOREVYN API

NOREVYN — Airspace Intelligence exposes the same HTTP API its own map and briefing UI runs on: live ADS-B traffic, FAA airspace and restriction data, aviation weather, drone-operation briefings, and continuous site monitoring. This page documents every public endpoint.

Overview

Rate limiting

Per-IP token buckets, in two API tiers. A bucket starts full, each request spends one token, and tokens refill continuously.

TierBurst capacityRefillApplies to
heavy40 requests0.6 / sec/api/states, /api/uas/brief, /api/uas/overlays, /api/airport/{code}, /api/geocode, /api/weather, /api/showcase, and POST /api/watch/sites — anything that can cost an upstream API call.
api90 requests2 / secEvery other /api/* route, including /api/airport/search, tracks, and the monitoring reads.

When a bucket is empty you get 429 with a retry-after: 30 header and {"error":"Too many requests — slow down."}. Back off and honour nextPollMs where the response provides one.

Conventions

Live data endpoints

GET/api/states

Live ADS-B state vectors for a viewport, from adsb.lol or OpenSky, with shared server-side caching so many clients cost one upstream request.

ParameterRequiredDescription
bboxnolamin,lomin,lamax,lomax. Omit for a worldwide snapshot (large; several MB gzipped). A malformed or degenerate box is a 400, not a silent global query. Viewports are rounded to 1° so nearby clients share a cache entry.
curl -s '/api/states?bbox=42.0,-83.6,42.7,-82.6'
{
  "time": 1787274674892,
  "source": "live",              // "live" | "sim"
  "provider": "adsblol",         // "adsblol" | "opensky" | null
  "providerLabel": "adsb.lol",
  "cached": false,               // true when served from the shared viewport cache
  "count": 26,
  "nextPollMs": 12000,           // poll no faster than this
  "credits": { "remaining": 400, "budget": 400, "authenticated": false },
  "error": null,                 // last upstream failure, if any
  "states": [
    {
      "icao24": "a42bd4", "callsign": "UPS2084", "country": null,
      "lastContact": 1787274675,        // unix seconds
      "lon": -83.589286, "lat": 42.054314,
      "baroAlt": 10675.62,              // metres; null when unknown
      "onGround": false,
      "speed": 245.85,                  // m/s ground speed
      "track": 93.6, "vertRate": 0,     // deg true; m/s
      "geoAlt": 11117.58, "squawk": "3142", "spi": false,
      "category": 6,                    // ADS-B emitter category (14 = UAV)
      "reg": "N368UP", "type": "B763",
      "airline": { "icao": "UPS", "name": "UPS Airlines", "iata": "5X" }
    } /* … */
  ]
}
US registrations (reg) are supplied by adsb.lol or derived from the ICAO 24-bit address for OpenSky data. airline is matched from the callsign prefix and may be null.

GET/api/track/{icao24}

Recent trail for one aircraft, recorded server-side from every viewport anyone has watched — tracks cost no extra upstream credits.

ParameterRequiredDescription
icao24yes (path)ICAO 24-bit address, hex, case-insensitive (e.g. a42bd4).
curl -s '/api/track/a42bd4'
{
  "icao24": "a42bd4",
  "points": [            // [timeMs, lat, lon, baroAltMetres, speedMs, trackDeg]
    [1787274674892, 42.054314, -83.589286, 10675.62, 245.85, 93.6]
    /* … up to 240 points, ~20 min retention */
  ]
}
Only aircraft recently seen through /api/states have points. An unknown or expired address returns 200 with an empty points array — not a 404. Trails are dropped ~20 minutes after last contact and do not survive a restart.

GET/api/airports

The bundled world airport list the UI seeds its map with (major fields only). Static — cache it client-side.

curl -s '/api/airports'
[
  { "iata": "DTW", "name": "Detroit Metropolitan Wayne County", "city": "Detroit",
    "cc": "US", "lat": 42.2124, "lon": -83.3534 } /* … */
]

Type-ahead airport search across live FAA airport records plus the bundled list. Cheap tier — it is designed to be hit per keystroke.

ParameterRequiredDescription
qyesIATA/FAA ident, city, or name fragment. Fewer than 2 characters returns {"results":[]}. Up to 20 results; FAA records win over builtin duplicates.
curl -s '/api/airport/search?q=detroit'
{
  "results": [
    {
      "globalId": "90EDCD5B-…", "ident": "DTW", "icao": "KDTW",
      "name": "Detroit Metro Wayne County", "city": "DETROIT",
      "state": "MI", "country": "UNITED STATES",
      "elevationFt": 645.2, "type": "AD", "military": "CIVIL",
      "privateUse": false, "status": "OPERATIONAL", "hasApproach": true,
      "lat": 42.2124, "lon": -83.3534,
      "source": "faa"            // "faa" | "builtin"
    } /* … */
  ]
}

GET/api/airport/{code}

A full operational picture for one airport: inferred live movements, runways, airspace, LAANC grid, TFRs, and weather. Heavy tier.

ParameterRequiredDescription
codeyes (path)FAA ident or IATA code (e.g. DTW). Case-insensitive. Unknown codes return 404 with {"error":"No airport found for \"…\""}.
curl -s '/api/airport/DTW'
{
  "airport": { "ident": "DTW", "icao": "KDTW", "name": "…", "lat": 42.21, "lon": -83.35, "elevationFt": 645.2,  },
  "source": "live", "time": 1787274674892,
  "movements": {
    "arrivals":    [ { "icao24": "…", "callsign": "DAL123", "reg": "N…", "airline": {…},
                       "altFt": 4200, "aglFt": 3555, "speedKt": 210, "vsFpm": -800,
                       "distNm": 12.4, "bearing": 220, "onGround": false,
                       "lat": 42.1, "lon": -83.1, "etaMin": 4 } ],
    "departures":  [  ], "ground": [  ], "overflights": [  ]
  },
  "counts": { "arrivals": 3, "departures": 2, "ground": 9, "overflights": 14 },
  "airspace": [  ], "uasfm": { "ceilingFt": 0,  },
  "runways": [  ],
  "tfrs": [  ], "tfrPending": 0,
  "weather": { "observation": {…}, "assessment": {…},  },
  "taf": {  }
}
ADS-B carries no flight plan, so arrivals/departures are inferred from altitude AGL, vertical rate, and whether the aircraft is closing on the field over its recorded trail, within 40 NM. Treat them as a picture, not a schedule.

GET/api/weather

Aviation weather (METAR/TAF) with a drone-operations assessment, by point or by station.

ParameterRequiredDescription
lat, lonyes*Point of interest, −90…90 / −180…180. Required unless station is given; otherwise 400 {"error":"lat and lon (or station) are required"}.
stationyes*ICAO station id (e.g. KDET). Returns that station's METAR + TAF + assessment; 404 if it has no current observation.
radiusnoSearch radius for nearby stations, NM. Default 30.
windLimit, gustLimitnoYour aircraft's wind/gust limits in kt — folded into the assessment verdict.
curl -s '/api/weather?lat=42.33&lon=-83.05'
{
  "observation": {
    "station": "KDET", "name": "Detroit/C Young Arpt, MI, US",
    "lat": 42.4072, "lon": -83.009, "elevFt": 623,
    "observedAt": "2026-08-21T00:53:00.000Z", "ageMin": 18,
    "tempC": 22.8, "dewpointC": 11.1, "windDir": 70, "windKt": 8, "gustKt": null,
    "visibilitySm": 10, "ceilingFt": null, "clouds": [],
    "altimeterHpa": 1018.7, "flightCategory": "VFR", "densityAltitudeFt": 1508,
    "raw": "METAR KDET 210053Z 07008KT 10SM CLR 23/11 A3008 …", "distNm": 5
  },
  "otherStations": [  ],
  "forecast": { … TAF, when available … },
  "assessment": { "verdict": "go", // "go" | "caution" | "no-go" | "unknown"
                  "findings": [  ], "maxAltitudeFt": null,  },
  "hazards": [ … SIGMETs etc … ]
}

GET/api/geocode

Address and place lookup: US Census geocoder for street addresses, OpenStreetMap Nominatim for places, plus raw lat, lon strings. Heavy tier — Nominatim allows 1 request/second, and NOREVYN enforces that server-side.

ParameterRequiredDescription
qyesFree-text query. Fewer than 3 characters returns {"results":[]}. Upstream failures return 200 with {"results":[], "error": "…"}.
curl -s '/api/geocode?q=1600+Pennsylvania+Ave+NW+Washington+DC'
{
  "results": [
    { "label": "1600 PENNSYLVANIA AVE NW, WASHINGTON, DC, 20500",
      "lat": 38.89869893252, "lon": -77.03518753691,
      "source": "census",      // "census" | "osm" | "coords"
      "kind": "address" }      // "address" | place type | "coordinates"
  ]
}

GET/api/uas/brief

A complete pre-flight airspace briefing for a drone operation at a point: LAANC/UASFM ceiling, controlled airspace, TFRs, national-security restrictions, special-use airspace, stadiums, live low-altitude traffic, weather — combined into one go / no-go verdict.

ParameterRequiredDescription
lat, lonyesOperation point. Missing, blank, or out-of-range values are a 400.
radiusnoTraffic scan radius, NM. Clamped to 1–25. Default 5.
altnoPlanned altitude, ft AGL, 1–2000. 0 or absent means "no altitude planned"; any other invalid value is a 400 ("alt must be a number between 1 and 2000 feet AGL."). With an altitude the verdict checks your operation against the binding limit, not just what exists at the point.
windLimit, gustLimitnoAircraft wind/gust limits in kt for the weather assessment.
aircraft, mission, timenoFree-text mission metadata (≤60 / ≤40 chars), echoed back in mission for record-keeping.
curl -s '/api/uas/brief?lat=42.3314&lon=-83.0458&radius=5&alt=200'
{
  "point": { "lat": 42.3314, "lon": -83.0458, "radiusNm": 5 },
  "source": "live",
  "uasfm": null,                       // UASFM grid cell here, or null
  "laanc": {
    "required": false, "maxAutoApprovalFt": null,
    "controllingAirspace": [], "overlyingAirspace": [ { "name": "DETROIT CLASS B",  } ]
  },
  "airspace": [ … every airspace feature at the point … ],
  "activeTfrs": [], "scheduledTfrs": [ { "notamId": "6/6868", "status": "scheduled",
    "startsInHours": 14.8, "type": "UAS PUBLIC GATHERING",  } ],
  "nearbyTfrs": [  ], "tfrPending": 0,
  "nsufr": [], "sua": [], "frias": [  ], "stadiums": [  ],
  "traffic": { "declared": [ … ADS-B category-14 UAVs … ],
               "lowAltitude": [ … manned aircraft below 2,000 ft … ] },
  "notams": { "configured": false, "items": [] },
  "weather": { … same shape as /api/weather … },
  "maxAltitude": {
    "ft": 400, "limitedBy": "Part 107 ceiling",
    "limits": [ { "source": "Part 107 ceiling", "ft": 400 } ],
    "uncertain": false, "uncertainReason": null
  },
  "verdict": {
    "level": "caution",   // go | caution | authorization-required | no-go | unknown
    "headline": "Clear with advisories",
    "primaryReason": "1 TFR scheduled over this point.",
    "reasons": [ { "level": "caution", "text": "…" } ]
  },
  "mission": { "plannedAltFt": 200, "aircraft": null, "missionType": null,
               "plannedTime": null, "generatedAt": "2026-08-21T01:11:14.000Z",  },
  "advisories": [ { "level": "warning", "text": "…" } ],
  "sourceErrors": {},      // per-source failure messages, when any
  "complete": true         // false when any FAA/weather source failed
}
Missing data never reads as clearance. If any upstream source fails, complete is false, the failed checks are named in sourceErrors, and the verdict becomes unknown ("Insufficient data") unless a confirmed blocker already made it no-go. Re-run the briefing before relying on it.

GET/api/uas/overlays

Map overlay geometry for a viewport: TFRs, the LAANC (UASFM) grid, national-security UAS restrictions, special-use airspace, FRIAs, and stadiums.

ParameterRequiredDescription
bboxyeslamin,lomin,lamax,lomax. Missing or malformed is 400 {"error":"bbox is required"}.
layersnoComma-separated: tfr (default), uasfm, nsufr, sua, fria, stadium.
curl -s '/api/uas/overlays?bbox=42.3,-83.2,42.5,-82.9&layers=tfr,uasfm,stadium'
{
  "tfrs": [  ], "tfrPending": 0, "tfrTotal": 129,
  "uasfm": [ … grid cells with ceilings … ],
  "stadiums": [  ],
  "errors": [ … present only if a requested layer failed … ]
}
The UASFM grid is ~1 sq mile per cell, so it is only returned when the bbox covers less than 1.2 square degrees. A wider view gets "uasfm": [] with "uasfmNote": "Zoom in further to load the LAANC grid."

GET/api/showcase

A small, heavily cached (5-minute) sample of current nationwide TFRs — what powers the landing page. Useful as a cheap "what's restricted right now" pulse.

curl -s '/api/showcase'
{
  "tfrTotal": 129, "activeCount": 79, "scheduledCount": 34, "pendingGeometry": 0,
  "tfrs": [
    { "notamId": "6/6931", "type": "HAZARDS", "status": "active",
      "startsInHours": -0.9, "city": "12NM N KETTLE FALLS", "state": "WA",
      "description": "…", "lowerFt": 0, "upperFt": 8000,
      "effective": "2026-08-21T00:15:00", "expires": "2026-08-31T04:00:00",
      "areas": [ { "type": "polygon", "points": [[48.899, -118.033], ] } ] }
    /* … up to 6, outlines simplified for thumbnails */
  ]
}

GET/api/sources

Upstream provider health, from a heartbeat sampled every 5 minutes — reading it never touches the providers themselves.

curl -s '/api/sources'
{
  "checkedAt": 1787274492975, "ageSec": 173,
  "adsb": { "preferred": "adsblol", "lastUsed": "adsblol",
            "openskyAuthenticated": false, "benched": {}, "notes": [] },
  "sources": [
    { "id": "adsb",    "role": "ADS-B traffic",  "vendor": "adsb.lol",              "ok": true, "status": 200, "ms": 433 },
    { "id": "uasfm",   "role": "LAANC ceilings", "vendor": "FAA UAS Facility Map",  "ok": true, "status": 200, "ms": 226 },
    { "id": "tfr",     "role": "Restrictions",   "vendor": "FAA TFR",               "ok": true, "status": 200, "ms": 393 },
    { "id": "weather", "role": "Weather",        "vendor": "NOAA Aviation Weather", "ok": true, "status": 200, "ms": 243 }
    /* … airspace, geocode … */
  ]
}

GET/api/status

Server self-status: data-source mode, upstream credit budget, cache sizes, and the monitoring loop's last cycle.

curl -s '/api/status'
{
  "authenticated": false, "forcedDemo": false,
  "credits": { "remaining": 400, "budget": 400, "used": 0 },
  "adsb": { "preferred": "adsblol",  },
  "monitor": { "sites": 2, "alertsRetained": 2, "cycleMinutes": 5,
               "lastCycle": { "at": 1787274508551, "checked": 2, "failed": 0, "ms": 4200 } },
  "trackedAircraft": 106, "recentFleet": 106, "cachedViewports": 3,
  "notamApiConfigured": false, "lastError": null, "uptimeSec": 178
}

GET/healthz

Liveness probe. Answered before rate limiting — safe for load-balancer health checks.

curl -s '/healthz'
{ "ok": true, "uptimeSec": 178 }

Monitoring API

Continuous site monitoring: register an operating site once and a server-side loop re-evaluates the airspace over it every 5 minutes, emitting an alert when something material changes — a TFR appears or goes active overhead, the LAANC ceiling moves, the weather verdict crosses a line, or checks degrade.

The token model

Durability, honestly: sites are persisted to a JSON file and survive process restarts, but on hosts that wipe the disk on deploy they do not survive a redeploy — keep your own copy of each registration and re-register if your sites disappear. Alert history is in-memory only and is lost on any restart (the alerts response says so itself). There is no SLA.

POST/api/watch/sites

Register a site for monitoring. Heavy tier. JSON body, max 16 KB (larger is 413; invalid JSON is 400).

Body fieldRequiredDescription
tokenyesYour owner token (format above), else 400 {"error":"invalid token"}.
lat, lonyesSite coordinates, −90…90 / −180…180.
namenoDisplay name, sanitized, ≤60 chars. Defaults to "Site {lat}, {lon}".
radiusNmnoClamped to 1–10 NM. Default 5.
plannedAltFtnoPlanned altitude, ft AGL; positive values only, capped at 2000, rounded. Anything else stored as null.
windLimitKtnoPositive, capped at 80 kt — feeds the weather verdict for this site.
gustLimitKtnoPositive, capped at 90 kt.

Other 400 errors: "limit is 5 sites", "monitoring is at capacity", and "this point is already monitored" (same token, same point within ~0.0001°; the existing site is returned alongside the error). On success the site gets an immediate baseline snapshot and a first info alert.

curl -s -X POST /api/watch/sites \
  -H 'content-type: application/json' \
  -d '{"token":"my-long-random-token-1","name":"Bridge site",
       "lat":42.3314,"lon":-83.0458,"radiusNm":5,"plannedAltFt":200}'
{
  "site": {
    "id": "d9275f3c12595c67",
    "name": "Bridge site", "lat": 42.3314, "lon": -83.0458,
    "radiusNm": 5, "plannedAltFt": 200,
    "createdAt": 1787274694119, "checkedAt": 0,
    "status": null            // populated after the baseline snapshot
  }
}

GET/api/watch/sites

List the sites owned by a token, each with its latest airspace snapshot.

ParameterRequiredDescription
tokenyesOwner token, else 400 {"error":"invalid token"}.
curl -s '/api/watch/sites?token=my-long-random-token-1'
{
  "sites": [
    {
      "id": "d9275f3c12595c67", "name": "Bridge site",
      "lat": 42.3314, "lon": -83.0458, "radiusNm": 5, "plannedAltFt": 200,
      "createdAt": 1787274694119, "checkedAt": 1787274694120,
      "status": {
        "at": 1787274694120,
        "tfrActive": [], "tfrScheduled": ["6/6868"],   // NOTAM ids overhead
        "hasGrid": false, "ceiling": null,             // LAANC grid + ceiling ft
        "wxVerdict": "go", "windKt": 8,
        "maxAlt": 400, "limitedBy": "Part 107",
        "unknown": []      // sources that failed this cycle — treat as unknown, not clear
      }
    }
  ]
}

GET/api/watch/alerts

Alerts for all of a token's sites, oldest first. Poll with since to get only what is new.

ParameterRequiredDescription
tokenyesOwner token.
sincenoEpoch milliseconds; only alerts strictly newer are returned. At most 100 per query.
curl -s '/api/watch/alerts?token=my-long-random-token-1&since=1787274000000'
{
  "alerts": [
    { "id": "23a695bfbddb", "siteId": "d9275f3c12595c67", "siteName": "Bridge site",
      "at": 1787274694119,
      "level": "info",       // "info" | "warning" | "critical"
      "text": "Monitoring started for Bridge site. Checks run every 5 minutes." }
  ],
  "retention": "in-memory — alert history does not survive a server restart"
}
Alerts fire only on material change between consecutive snapshots: a TFR becoming active or newly scheduled overhead (and one when it clears), a LAANC ceiling moving or the site entering/leaving a grid, the weather verdict crossing go/caution/no-go, and one alert per transition into or out of degraded checking. A failed upstream check is excluded from diffing entirely, so an outage can never masquerade as "restriction lifted".

POST/api/watch/sites/delete

Stop monitoring a site. Deletion is by POST (not DELETE) so the whole API stays proxy- and form-friendly.

Body fieldRequiredDescription
tokenyesOwner token. Must match the site's token.
idyesSite id from create/list.
curl -s -X POST /api/watch/sites/delete \
  -H 'content-type: application/json' \
  -d '{"token":"my-long-random-token-1","id":"d9275f3c12595c67"}'
{ "ok": true }
// wrong token or unknown id → 404 { "error": "no such site" }

Fair use & disclaimer