The tile server listens on port 8600 by default. All endpoints support CORS (Access-Control-Allow-Origin: *).
Returns a colorized 256x256 PNG radar tile.
Path parameters:
| Parameter | Description |
|---|---|
timestamp |
Frame timestamp in YYYYMMDDHHMMSS format (e.g. 20260322143000) |
z |
Tile zoom level (integer) |
x |
Tile column (integer) |
y |
Tile row (integer, XYZ convention) |
Query parameters:
| Parameter | Default | Description |
|---|---|---|
palette |
default |
Color palette name. See Palettes for available names. |
source |
composite |
Data source name. See Sources or GET /sources. |
layer |
reflectivity |
NEXRAD data layer at z8+. One of reflectivity, biological, velocity. Ignored below z8. |
Response: 200 image/png — a 256x256 RGBA PNG tile.
If no tile exists for the given coordinates, returns a 1x1 transparent PNG (68 bytes) rather than 404. This allows map libraries to fail silently for tiles outside the radar coverage area.
If the palette is unknown, returns 400 application/json:
{ "error": "Unknown palette: badname" }Cache headers:
- Latest frame:
Cache-Control: public, max-age=60 - Historical frames:
Cache-Control: public, max-age=86400, immutable
Response headers:
| Header | Values | Description |
|---|---|---|
X-Cache |
hit, miss |
Whether the tile came from in-memory LRU cache or MBTiles |
Content-Type |
image/png |
Always PNG |
Example:
GET /tile/20260322143000/6/14/26?palette=dark&source=composite
At zoom levels 8 and above, tile requests may be served from NEXRAD Level 2 station data (250 m resolution) rather than MRMS composite tiles, depending on which source is active for the requested source parameter.
Returns available frame timestamps for a source, newest first.
Query parameters:
| Parameter | Default | Description |
|---|---|---|
source |
composite |
Source name |
limit |
720 |
Maximum number of frames to return (capped at 2000) |
since |
0 |
Return only frames with epochMs greater than this value (Unix ms). Use for incremental polling. 0 returns all frames. |
Response: 200 application/json
{
"source": "composite",
"frames": [
{ "timestamp": "20260322141000", "epochMs": 1742651400000 },
{ "timestamp": "20260322143000", "epochMs": 1742651520000 }
],
"latest": "20260322143000",
"count": 2
}Frames are returned in ascending chronological order (oldest first), making them suitable for driving animation loops.
Example:
GET /frames?source=mrms&limit=30
GET /frames?source=composite&since=1742651000000
Returns the most recent frame for a source.
Query parameters:
| Parameter | Default | Description |
|---|---|---|
source |
composite |
Source name |
Response: 200 application/json
{
"timestamp": "20260322143000",
"epochMs": 1742651520000,
"source": "composite",
"age": 87
}age is seconds since the frame's observation time (not since ingestion). Values below 300 seconds indicate a healthy, up-to-date source.
Response on no data: 503 application/json
{ "error": "No frames available yet" }Example:
GET /frames/latest?source=mrms
Returns the list of available source names.
Response: 200 application/json
{
"sources": [
{ "name": "composite", "description": "Global (all sources)" },
{ "name": "composite-na", "description": "North America" },
{ "name": "composite-eu", "description": "Europe" },
{ "name": "mrms", "description": "SeamlessHSR_00.00" },
{ "name": "mrms-alaska", "description": "SeamlessHSR_00.00" },
{ "name": "mrms-hawaii", "description": "MergedBaseReflectivity_00.50" },
{ "name": "ec", "description": "RADAR_1KM_RRAI" },
{ "name": "dwd", "description": "hx" }
]
}Type sources (mrms-type, ec-type, etc.) are excluded from this list. They are referenced implicitly when using the precip-type palette.
Example:
GET /sources
Returns the list of available color palettes.
Response: 200 application/json
{
"palettes": [
{ "name": "default", "description": "Standard weather radar" },
{ "name": "noaa", "description": "NWS official style" },
{ "name": "dark", "description": "High contrast for dark maps" },
{ "name": "viridis", "description": "Colorblind-safe (perceptually uniform)" },
{ "name": "grayscale", "description": "Raw dBZ as grayscale intensity" },
{ "name": "precip-type", "description": "Precipitation type (rain/snow/ice/hail)" }
]
}Example:
GET /palettes
Returns a 30x256 RGBA PNG color legend for the named palette. The top of the image corresponds to pixel value 255 (80 dBZ maximum) and the bottom to pixel value 0 (NoData, transparent).
Path parameters:
| Parameter | Description |
|---|---|
name |
Palette name (e.g. default, dark, noaa) |
Response: 200 image/png with Cache-Control: public, max-age=86400, immutable
Response on unknown palette: 404 application/json
{ "error": "Palette not found: badname" }Example:
GET /palette/default/legend
GET /palette/noaa/legend
Returns the operational status of all data sources.
Response: 200 application/json
{
"status": "ok",
"sources": {
"mrms": {
"status": "ok",
"lastFrame": "20260322143000",
"ageSeconds": 87,
"consecutiveErrors": 0
},
"mrms-alaska": {
"status": "ok",
"lastFrame": "20260322143000",
"ageSeconds": 102,
"consecutiveErrors": 0
},
"dwd": {
"status": "stale",
"lastFrame": "20260322140000",
"ageSeconds": 542,
"consecutiveErrors": 2
}
},
"latestComposite": "20260322143000",
"uptimeSeconds": 3847
}Status values:
| Value | Meaning |
|---|---|
ok |
Last successful fetch was less than 5 minutes ago |
stale |
Last successful fetch was 5+ minutes ago |
unknown |
No successful fetch recorded yet (e.g. source just started) |
degraded (top-level) |
No sources are ok |
Example:
GET /health
Returns Prometheus-format metrics for monitoring.
Response: 200 text/plain; charset=utf-8
radrview_source_frame_age_seconds{source="mrms"} 87.3
radrview_source_frame_age_seconds{source="mrms-alaska"} 102.1
radrview_source_frame_age_seconds{source="dwd"} 542.0
radrview_frames_available{source="mrms"} 720
radrview_frames_available{source="composite"} 720
radrview_tile_cache_hits_total 14823
radrview_tile_cache_misses_total 3041
radrview_tile_serve_p50_ms 3.2
radrview_tile_serve_p95_ms 18.7
radrview_tile_serve_p99_ms 45.1
radrview_uptime_seconds 3847
Metrics:
| Metric | Description |
|---|---|
radrview_source_frame_age_seconds{source} |
Seconds since the source last ingested a frame. -1 if no data yet. |
radrview_frames_available{source} |
Number of frames in the sorted set for this source. |
radrview_tile_cache_hits_total |
Total LRU cache hits since server start. |
radrview_tile_cache_misses_total |
Total LRU cache misses since server start. |
radrview_tile_serve_p50_ms |
50th percentile tile serve latency (ms). |
radrview_tile_serve_p95_ms |
95th percentile tile serve latency (ms). |
radrview_tile_serve_p99_ms |
99th percentile tile serve latency (ms). |
radrview_uptime_seconds |
Server uptime in seconds. |
Example:
GET /metrics
Returns the list of all NEXRAD WSR-88D stations with their current status and data age.
Response: 200 application/json
{
"stations": [
{
"stationId": "KLOT",
"name": "Chicago/Romeoville",
"lat": 41.604,
"lon": -88.085,
"status": "active",
"ageMinutes": 4.2
},
{
"stationId": "KIWX",
"name": "North Webster",
"lat": 41.358,
"lon": -85.700,
"status": "stale",
"ageMinutes": 18.7
},
{
"stationId": "KGRR",
"name": "Grand Rapids",
"lat": 42.894,
"lon": -85.545,
"status": "unavailable",
"ageMinutes": null
}
],
"count": 159
}Status values:
| Value | Meaning |
|---|---|
active |
Latest volume scan is less than 10 minutes old (green marker) |
stale |
Latest volume scan is 10–60 minutes old (orange marker) |
unavailable |
No volume scan received or data is older than 60 minutes (red marker) |
ageMinutes is null for stations with no data. The frontend uses this endpoint to color-code station markers and show age tooltips on hover.
Example:
GET /nexrad/stations
Connect to receive real-time notifications when new radar frames are available and to stream NEXRAD real-time sweep data.
Connection:
wss://radrview.com/ws
Outgoing messages (client → server):
Send a viewport message to subscribe to sweep-wedge updates for stations within the current map view:
{
"type": "viewport",
"west": -90.5,
"south": 40.1,
"east": -85.2,
"north": 43.8,
"zoom": 9
}The server uses the viewport to limit which stations' sweep-wedge events are forwarded to this client.
Incoming messages (server → client):
new-frame — emitted when the compositor produces a new composite frame:
{
"type": "new-frame",
"timestamp": "20260322143000",
"epochMs": 1742651520000,
"source": "composite"
}The server only broadcasts new-frame events for the main composite source. Clients should call GET /frames/latest?source=composite after receiving this event to get full frame metadata, then request tile URLs for the new timestamp.
sweep-wedge — emitted progressively as each 60° wedge of a live NEXRAD scan completes (only for stations with real-time chunk data):
{
"type": "sweep-wedge",
"stationId": "KLOT",
"stationLat": 41.604,
"stationLon": -88.085,
"volumeId": "20260322143012",
"azStart": 0,
"azEnd": 60,
"radials": [
{ "az": 0.5, "gates": [/* dBZ values array, 460 km range */] },
{ "az": 1.0, "gates": [...] }
]
}azStart/azEnd are degrees (0–360). The frontend renders each wedge onto a Canvas overlay as it arrives, producing a rotating sweep line animation. A full 360° volume produces 6 wedge messages.
Example (browser):
const ws = new WebSocket('wss://radrview.com/ws');
ws.onopen = () => {
// Subscribe to sweeps for the current map viewport
ws.send(JSON.stringify({
type: 'viewport',
west: -90.5, south: 40.1, east: -85.2, north: 43.8, zoom: 9
}));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'new-frame') {
console.log('New frame available:', msg.timestamp);
} else if (msg.type === 'sweep-wedge') {
console.log(`Sweep wedge from ${msg.stationId}: ${msg.azStart}°–${msg.azEnd}°`);
}
};The WebSocket connection has no heartbeat. Clients should implement reconnection logic.
Returns the current GFS 10m wind field as a global grid. Data is refreshed every 6 hours from NOAA NOMADS.
Response: 200 application/json
{
"u": "<base64-encoded Float32Array>",
"v": "<base64-encoded Float32Array>",
"width": 1440,
"height": 721,
"lonMin": 0,
"lonMax": 359.75,
"latMin": -90,
"latMax": 90,
"dx": 0.25,
"dy": 0.25,
"timestamp": 1742651400000,
"cycle": "20260322/00z"
}u and v are east-west and north-south wind components in m/s, encoded as base64 Float32Arrays (~8 MB total). The grid is row-major with (0,0) at lat -90, lon 0. The frontend uses these to animate wind flow particles on a Canvas overlay.
Response on no data: 503 application/json
{ "error": "Wind data not yet available" }Example:
GET /wind/grid
The aviation situation API runs as a separate service on port 8601 and provides real-time weather intelligence for airports and flight routes. All endpoints support CORS.
Returns a regional summary of weather conditions across all monitored regions, including affected airports and system status.
Response: 200 application/json
{
"generated": "2026-04-09T05:40:00.000Z",
"dataAge": 44,
"regions": [
{
"id": "northeast-corridor",
"label": "Northeast Corridor",
"bounds": { "north": 45, "south": 38, "east": -70, "west": -79 },
"maxDbz": 42,
"coveragePct": 12.5,
"precipTypes": ["rain"],
"severity": "moderate",
"trend": "intensifying",
"affectedAirports": ["KJFK", "KEWR"]
}
],
"systemStatus": "operational"
}Regions: Northeast Corridor, Southeast, Midwest, South Central, Mountain West, Pacific West, Western Europe, Central Europe.
systemStatus values:
| Value | Meaning |
|---|---|
operational |
Composite data is current |
degraded |
Composite data is moderately stale |
offline |
Composite data is severely stale or unavailable |
Response on no data: 503 application/json
{ "error": "Summary not yet computed" }Example:
GET /situation/summary
Returns the current weather situation for a specific airport, including ring-based radar sampling, ramp status, and trend analysis.
Path parameters:
| Parameter | Description |
|---|---|
icao |
4-letter ICAO airport code (case-insensitive, e.g. KORD, kord) |
Response: 200 application/json
{
"icao": "KORD",
"timestamp": "2026-04-09T05:34:56.000Z",
"dataAge": 44,
"rings": {
"5nm": { "maxDbz": 0, "precipTypes": [], "severity": "clear" },
"20nm": { "maxDbz": 35, "precipTypes": [], "severity": "moderate" },
"50nm": { "maxDbz": 44, "precipTypes": [], "severity": "moderate" }
},
"trend": "steady",
"rampStatus": "clear",
"nearestActiveCell": {
"distanceNm": 19.3,
"bearing": 308,
"dbz": 35
}
}Ring radii: 5 nm (airfield), 20 nm (terminal area), 50 nm (approach/departure corridors).
severity values:
| Value | dBZ Range |
|---|---|
clear |
< 20 |
light |
20–34 |
moderate |
35–49 |
heavy |
50–59 |
extreme |
60+ |
rampStatus values:
| Value | Meaning |
|---|---|
clear |
No ramp restrictions expected |
caution |
Freezing precip nearby or moderate dBZ on airfield |
suspend |
Hail detected or heavy dBZ on airfield |
trend values:
| Value | Meaning |
|---|---|
intensifying |
50nm max dBZ increasing significantly |
weakening |
50nm max dBZ decreasing significantly |
steady |
No significant change |
developing |
Was clear, now has activity |
clearing |
Activity dissipating to clear |
unknown |
No prior frame to compare |
nearestActiveCell is null when no active cells are within 50 nm. bearing is degrees true (0 = north, 90 = east).
Response on unknown airport: 404 application/json
{ "error": "Airport not found: ZZZZ" }Example:
GET /situation/airport/KORD
GET /situation/airport/EGLL
Returns time-series history frames for a watched airport. History is only available for airports actively subscribed via WebSocket.
Path parameters:
| Parameter | Description |
|---|---|
icao |
4-letter ICAO airport code (case-insensitive) |
Query parameters:
| Parameter | Default | Description |
|---|---|---|
hours |
3 |
Number of hours of history to return (1–24) |
Response: 200 application/json
{
"icao": "KORD",
"hours": 3,
"frames": [
{
"timestamp": "2026-04-09T03:30:00.000Z",
"rings": {
"5nm": { "maxDbz": 0, "precipTypes": [], "severity": "clear" },
"20nm": { "maxDbz": 22, "precipTypes": ["rain"], "severity": "light" },
"50nm": { "maxDbz": 38, "precipTypes": ["rain"], "severity": "moderate" }
},
"rampStatus": "clear"
}
]
}Response when airport is not watched: 404 application/json
{ "error": "Airport KORD is not on the watchlist. History is only available for watched airports." }Example:
GET /situation/airport/KORD/history?hours=6
Samples radar data along a multi-waypoint flight route and returns per-segment weather analysis with severity and recommendations.
Query parameters:
| Parameter | Description |
|---|---|
waypoints |
Comma-separated ICAO codes (minimum 2, case-insensitive). e.g. KORD,KJFK |
Response: 200 application/json
{
"waypoints": ["KORD", "KJFK"],
"timestamp": "2026-04-09T05:34:56.000Z",
"segments": [
{
"from": "KORD",
"to": "KJFK",
"distanceNm": 634,
"maxDbzAlongRoute": 42,
"significantCells": 2,
"severity": "moderate",
"recommendation": "deviations possible",
"samplePoints": [
{ "lat": 41.97, "lon": -87.91, "distanceNm": 0, "maxDbz": 0, "severity": "clear" },
{ "lat": 42.15, "lon": -86.82, "distanceNm": 50, "maxDbz": 35, "severity": "moderate" }
]
}
]
}Route is sampled every 50 nm with a 3x3 pixel neighborhood for max dBZ.
recommendation values:
| Value | Meaning |
|---|---|
clear |
No weather concerns |
monitor |
Weather present, monitor conditions |
deviations possible |
May need to deviate from planned route |
deviations likely |
Deviations expected |
avoid segment |
Severe weather, avoid this segment |
Response on validation error: 400 application/json
{ "error": "At least 2 waypoints required" }{ "error": "Unknown airports: ZZZZ" }Example:
GET /situation/route?waypoints=KORD,KDFW,KMIA
GET /situation/route?waypoints=EGLL,EDDF
Returns detected storm cells as a GeoJSON FeatureCollection for map overlay rendering.
Query parameters:
| Parameter | Default | Description |
|---|---|---|
threshold |
35 |
Minimum dBZ to consider a cell (integer) |
bounds |
(none) | Geographic filter: north,south,east,west (decimal degrees) |
Response: 200 application/json — GeoJSON FeatureCollection with cell polygon features.
Each feature's properties include:
| Property | Description |
|---|---|
maxDbz |
Peak reflectivity in the cell |
severity |
Severity classification |
precipType |
Dominant precipitation type |
areaKm2 |
Approximate cell area in km² |
Example:
GET /overlays/cells.geojson
GET /overlays/cells.geojson?threshold=45&bounds=45,38,-70,-79
Real-time aviation weather alerts for watched airports. Connects to the situation API service on port 8601.
Connection:
wss://radrview.com/ws/aviation
Subscribe (client → server):
{
"type": "subscribe",
"clientId": "my-app-123",
"watchlist": ["KORD", "KJFK", "KATL"],
"thresholds": {
"dbz": 20,
"precipTypes": []
}
}ICAO codes are normalized to uppercase. Only valid ICAO codes are accepted; unknown codes are silently dropped. Airports on the watchlist are sampled on each new composite frame and their history is retained for up to 24 hours.
Incoming messages (server → client):
condition-change — ramp status or severity changed at a watched airport:
{
"type": "condition-change",
"icao": "KORD",
"timestamp": "2026-04-09T05:40:00.000Z",
"previous": { "severity": "clear", "rampStatus": "clear" },
"current": { "severity": "moderate", "rampStatus": "caution" },
"trend": "developing"
}Messages are filtered by the client's thresholds.dbz — changes below the threshold are suppressed.
all-clear — conditions returned to clear at a watched airport:
{
"type": "all-clear",
"icao": "KORD",
"timestamp": "2026-04-09T06:15:00.000Z",
"rampStatus": "clear"
}data-stale — composite data has not updated within the expected interval:
{
"type": "data-stale",
"ageSeconds": 420,
"affectedSources": ["mrms"]
}Example (browser):
const ws = new WebSocket('wss://radrview.com/ws/aviation');
ws.onopen = () => {
ws.send(JSON.stringify({
type: 'subscribe',
clientId: 'dispatch-board',
watchlist: ['KORD', 'KJFK', 'KATL', 'KDFW'],
thresholds: { dbz: 20, precipTypes: [] }
}));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
switch (msg.type) {
case 'condition-change':
console.log(`${msg.icao}: ${msg.previous.rampStatus} → ${msg.current.rampStatus}`);
break;
case 'all-clear':
console.log(`${msg.icao}: all clear`);
break;
case 'data-stale':
console.warn(`Data stale: ${msg.ageSeconds}s`);
break;
}
};The server sends periodic pings (30s interval). Clients that fail to respond are disconnected.
For use with mapping libraries (Leaflet, MapLibre, OpenLayers):
https://radrview.com/tile/{timestamp}/{z}/{x}/{y}?palette=default&source=composite
Leaflet example:
L.tileLayer('https://radrview.com/tile/20260322143000/{z}/{x}/{y}?palette=dark', {
opacity: 0.7,
tms: false,
}).addTo(map);MapLibre source:
{
"type": "raster",
"tiles": ["https://radrview.com/tile/20260322143000/{z}/{x}/{y}?palette=default"],
"tileSize": 256
}