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
- Base URL — all paths are relative to the origin serving this page (e.g.
/api/states). There is no separate API host or version prefix. - No authentication — no API keys, no accounts. JSON responses carry
Access-Control-Allow-Origin: *, so you can call the API straight from a browser. - Methods — everything is
GET/HEAD, except the two monitoring write endpoints, which arePOSTwith a JSON body. Anything else returns405. - Format — JSON,
cache-control: no-store, gzip-compressed when the body exceeds 4 KB and the client accepts it. URLs longer than 2048 characters are rejected with414. - Errors — non-2xx responses are JSON with a single
errorstring, e.g.{"error":"bbox is required"}.
Rate limiting
Per-IP token buckets, in two API tiers. A bucket starts full, each request spends one token, and tokens refill continuously.
| Tier | Burst capacity | Refill | Applies to |
|---|---|---|---|
| heavy | 40 requests | 0.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. |
| api | 90 requests | 2 / sec | Every 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
- Coordinates — decimal degrees, WGS-84.
latmust be within −90…90 andlonwithin −180…180; out-of-range or non-numeric values are rejected, never silently coerced. - Bounding boxes —
bbox=lamin,lomin,lamax,lomax(south, west, north, east). Values are clamped to the valid range; after clamping,lamin<lamaxandlomin<lomaxmust hold or the request is a400. - Units — raw state vectors use SI (altitude in metres, speed and vertical rate in m/s); derived briefing fields are labelled in aviation units (
altFt,speedKt,vsFpm,distNm). Timestamps are Unix epoch milliseconds unless the field is namedlastContact(seconds) or is an ISO-8601 string. - Live vs simulated — when no live provider is reachable the server substitutes a clearly-labelled deterministic simulator. Always check
source:"live"or"sim". Simulated aircraft additionally carry"simulated": true.
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.
| Parameter | Required | Description |
|---|---|---|
| bbox | no | lamin,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" }
} /* … */
]
}
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.
| Parameter | Required | Description |
|---|---|---|
| icao24 | yes (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 */
]
}
/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 } /* … */
]
GET/api/airport/search
Type-ahead airport search across live FAA airport records plus the bundled list. Cheap tier — it is designed to be hit per keystroke.
| Parameter | Required | Description |
|---|---|---|
| q | yes | IATA/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.
| Parameter | Required | Description |
|---|---|---|
| code | yes (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": { … }
}
GET/api/weather
Aviation weather (METAR/TAF) with a drone-operations assessment, by point or by station.
| Parameter | Required | Description |
|---|---|---|
| lat, lon | yes* | Point of interest, −90…90 / −180…180. Required unless station is given; otherwise 400 {"error":"lat and lon (or station) are required"}. |
| station | yes* | ICAO station id (e.g. KDET). Returns that station's METAR + TAF + assessment; 404 if it has no current observation. |
| radius | no | Search radius for nearby stations, NM. Default 30. |
| windLimit, gustLimit | no | Your 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.
| Parameter | Required | Description |
|---|---|---|
| q | yes | Free-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.
| Parameter | Required | Description |
|---|---|---|
| lat, lon | yes | Operation point. Missing, blank, or out-of-range values are a 400. |
| radius | no | Traffic scan radius, NM. Clamped to 1–25. Default 5. |
| alt | no | Planned 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, gustLimit | no | Aircraft wind/gust limits in kt for the weather assessment. |
| aircraft, mission, time | no | Free-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
}
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.
| Parameter | Required | Description |
|---|---|---|
| bbox | yes | lamin,lomin,lamax,lomax. Missing or malformed is 400 {"error":"bbox is required"}. |
| layers | no | Comma-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 … ]
}
"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
- There are no accounts. You generate an owner token yourself; the token is the account. Every operation is scoped to it, and the API never lists tokens back.
- Format: 8–64 characters of lowercase letters, digits, and hyphens, not starting with a hyphen (
^[a-z0-9][a-z0-9-]{7,63}$). On GET requests, send the token as anX-NOREVYN-Tokenheader instead of a query parameter where you can — query strings tend to end up in proxy logs. Use something long and random — anyone holding the token controls its sites. - Lose the token, lose access. There is no recovery, and the server cannot tell you what your token was.
- Caps: 5 sites per token, 500 sites server-wide, 2000 alerts retained globally, 100 alerts per query.
POST/api/watch/sites
Register a site for monitoring. Heavy tier. JSON body, max 16 KB (larger is 413; invalid JSON is 400).
| Body field | Required | Description |
|---|---|---|
| token | yes | Your owner token (format above), else 400 {"error":"invalid token"}. |
| lat, lon | yes | Site coordinates, −90…90 / −180…180. |
| name | no | Display name, sanitized, ≤60 chars. Defaults to "Site {lat}, {lon}". |
| radiusNm | no | Clamped to 1–10 NM. Default 5. |
| plannedAltFt | no | Planned altitude, ft AGL; positive values only, capped at 2000, rounded. Anything else stored as null. |
| windLimitKt | no | Positive, capped at 80 kt — feeds the weather verdict for this site. |
| gustLimitKt | no | Positive, 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.
| Parameter | Required | Description |
|---|---|---|
| token | yes | Owner 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.
| Parameter | Required | Description |
|---|---|---|
| token | yes | Owner token. |
| since | no | Epoch 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"
}
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 field | Required | Description |
|---|---|---|
| token | yes | Owner token. Must match the site's token. |
| id | yes | Site 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
- Situational awareness only — never authorization. Nothing this API returns is a clearance, a LAANC authorization, or legal advice. A
"go"verdict means the sources NOREVYN could reach showed no blocker at that moment; it is the start of your pre-flight process, not the end. Obtain LAANC authorization through an FAA-approved provider, check official NOTAMs, and follow 14 CFR Part 107 (or your applicable rules) yourself. - Data provenance. Traffic comes from adsb.lol and/or the OpenSky Network (community ADS-B — coverage gaps and dropouts are normal); airspace, UASFM/LAANC grids, TFRs, and related layers from FAA public services; weather from NOAA aviationweather.gov; geocoding from the US Census and OpenStreetMap Nominatim. All of it can be delayed, incomplete, or wrong, and each provider's own terms apply to your use of their data.
- Check
source. When live traffic is unreachable the server substitutes a clearly-labelled simulator ("source":"sim"). Never treat simulated traffic as real. - Be gentle. The heavy endpoints proxy free upstream services with real quotas. Respect the rate limits, poll
/api/statesno faster than thenextPollMsit returns, and cache anything static (/api/airports,/api/showcase). Sustained abuse gets your IP throttled at429. - No SLA, no warranty. This service is provided as-is: alert history is lost on every restart, monitored sites may be lost on redeploy, endpoints and response shapes may change, and availability is not guaranteed. Do not make it a single point of failure for flight-safety decisions.