Windfield

see how wind moves over real terrain

Guide

Everything you need to use Windfield, plus the full public API for anyone writing code or an AI agent against it.

Quick start

  1. Draw an area. Click the draw-area tool (or press D), then drag a box on the map — that's the ground WindNinja will model. You can also search for a place first and use its area, or "use current view".
  2. Choose how the wind starts. Pick one of the three big cards: Simulate (you set the wind by hand), Current conditions (live weather fills it in automatically), or Forecast (a weather model drives it over several hours, with a time player).
  3. Set options. Speed/direction/height (Simulate), vegetation, mesh detail (how fine the grid is), and anything else the mode needs. Every control has a short (i) explanation next to it.
  4. Run. Press the Run button (or R). The app shows a live estimate (cells, seconds, terrain resolution) before you run, and progress while it runs.
  5. Read the result. A colour speed raster drapes over the terrain, with arrows, optional animated particles, a legend and stats. Click anywhere to probe the exact speed/direction/elevation at that point.

What runs where

All of the modelling happens on the server: downloading and building the terrain, running WindNinja, and producing the speed raster, arrows and grid. Your browser never runs the wind model — it only draws the area you pick, sends the request, polls for progress, and renders the images and GeoJSON the server sends back. This is why a run keeps progressing even on a slow phone, and why the same job id gives the same result to anyone who opens the link while it's still alive.

Reading the wind

Structures

Draw a building, wall, barn or any polygon/rectangle/circle/ellipse shape on the map, give it a height (or mark it infinitely tall), and the server raises the terrain under it before solving. This is a good way to see how wind deflects around one specific object rather than just open terrain.

Two things matter for getting a useful result:

Live weather and data sources

SourceWhat it providesAvailability
Open-MeteoCurrent conditions and short-range hourly forecast (speed, direction, gust, temperature, humidity, cloud, pressure) for any point on Earth.Worldwide
NOAA NWS stationsReal station observations and active weather alerts.United States only
METAR airportsAviation weather station observations — wind, temperature, visibility.Worldwide
NDBC buoysCoastal and offshore marine wind observations.Coastal/ocean only, 0 results inland
RadiosondesUpper-air soundings used to build a vertical wind profile with height.Near ~55 North American launch sites, depends on the latest flight having reported
GFS / HRRR / NAM (NOAA NOMADS)Forecast-model wind fields that drive Forecast mode, one WindNinja run per forecast hour.GFS worldwide (28 km); HRRR and NAM are CONUS (continental US) only, at 3 km and 12 km
OSM NominatimPlace search and reverse lookup (addresses, zip/postal codes, cities, mountains, buildings, countries).Worldwide

Some sources depend on optional server configuration (for example a RAWS/mesonet network needs a free API token) and report themselves as unavailable with a reason, rather than silently returning nothing.

Limits and fair use

There's no fixed area-size cutoff. Instead every run is budgeted by cells, time and memory, reported live by GET /api/v1/capabilities:

Please keep automated/API request rates light. See the full policy in /ai.txt and /agents.txt.

Keyboard shortcuts

DDraw area
SAdd structure
PProbe point
MMeasure
XCross-section
VSelect / edit
LToggle layers panel
RRun
/Search
EscExit the current tool / mode
DeleteRemove the selected object
Alt+1…9Jump to window 1–9

Privacy

Windfield has no login and no accounts. Its privacy policy and terms are SummitFlux's own: Privacy policy · Terms.

API (for developers and AI agents)

Base URL https://windfield.summitflux.com/api/v1. JSON, CORS open, no auth, no key. Attribution is requested, not required — see /ai.txt. Full machine-readable references: /agents.txt, /agents.xml, /llms.txt, /llms-full.txt.

GET /api/v1/health

{
  "ok": true,
  "windninja": "4.0.0",
  "queue": { "running": 1, "queued": 0 },
  "limits": { "max_domain_km": 400, "max_cells": 1500000, "max_run_seconds": 900 }
}

GET /api/v1/capabilities

{
  "mesh_choices": ["coarse", "medium", "fine"],
  "mesh_choice_cells": { "coarse": 4000, "medium": 10000, "fine": 20000 },
  "mesh_resolution_m": { "min": 10, "max": 2000 },
  "vegetation": ["grass", "brush", "trees"],
  "max_domain_km": 400,
  "max_cells": 1500000,
  "max_run_seconds": 900,
  "initialization": ["domain_average", "point", "weather_model", "profile"],
  "wx_models": [
    { "id": "NOMADS-GFS-GLOBAL-0.25-DEG", "label": "GFS Global 0.25°", "coverage": "global" },
    { "id": "NOMADS-HRRR-CONUS-3-KM", "label": "HRRR CONUS 3 km", "coverage": "conus" },
    { "id": "NOMADS-NAM-CONUS-12-KM", "label": "NAM CONUS 12 km", "coverage": "conus" }
  ],
  "solvers": [
    { "id": "mass", "label": "Fast (mass-conserving)", "eta": "seconds", "available": true },
    { "id": "momentum", "label": "High fidelity (momentum / OpenFOAM) -- models wakes behind buildings",
      "eta": "2-10 min", "available": false, "max_cells": 60000 }
  ]
}

POST /api/v1/jobs

Domain-average Simulate run, with output heights and one structure:

{
  "bbox": [-105.72, 39.54, -105.58, 39.64],
  "initialization": "domain_average",
  "input_speed": 15, "input_speed_units": "mph",
  "input_direction": 270,
  "input_wind_height": 10, "input_wind_height_units": "m",
  "output_wind_height": 10, "output_wind_height_units": "m",
  "output_heights": [2, 10, 30],
  "vegetation": "grass",
  "mesh_choice": "medium",
  "structures": [
    { "id": "s1", "label": "Barn", "height_m": 12, "infinite": false, "mode": "raise",
      "geometry": { "type": "Polygon", "coordinates": [[[-105.651,39.601],[-105.650,39.601],[-105.650,39.602],[-105.651,39.602],[-105.651,39.601]]] } }
  ],
  "outputs": ["geojson", "png", "geotiff"]
}

Point initialization (station-fed wind):

{
  "bbox": [-105.72, 39.54, -105.58, 39.64],
  "initialization": "point",
  "mesh_choice": "coarse",
  "points": [
    { "lat": 39.60, "lon": -105.65, "speed": 12, "direction": 260, "height": 10 }
  ]
}

Forecast run (weather model):

{
  "bbox": [-105.72, 39.54, -105.58, 39.64],
  "initialization": "weather_model",
  "wx_model": "NOMADS-HRRR-CONUS-3-KM",
  "forecast_hours": 6,
  "mesh_choice": "coarse"
}

Vertical-profile run:

{
  "bbox": [-105.72, 39.54, -105.58, 39.64],
  "initialization": "profile",
  "profile": [
    { "height_m": 2, "speed": 5, "direction": 270 },
    { "height_m": 100, "speed": 25, "direction": 300 }
  ],
  "output_heights": [2, 50, 100],
  "mesh_choice": "coarse"
}

Response:

201 { "id": "01J...ULID", "status": "queued", "status_url": "/api/v1/jobs/01J...ULID" }

GET /api/v1/jobs/{id}

{
  "id": "01J...ULID", "status": "done", "progress": 100,
  "message": null,
  "created_at": "2026-10-06T14:00:00Z",
  "started_at": "2026-10-06T14:00:01Z",
  "finished_at": "2026-10-06T14:00:29Z",
  "expires_in_s": 298,
  "params": { "...": "echo of the request" },
  "dem": { "source": "AWS Terrarium (Mapzen/SRTM/...)", "resolution_m": 100, "cells": [629, 694], "bbox": [-105.72, 39.54, -105.58, 39.64] },
  "results": {
    "speed_units": "mph", "height_m": 10,
    "bounds": [[39.54, -105.72], [39.64, -105.58]],
    "png": "/api/v1/jobs/01J.../files/speed.png",
    "legend": [ { "min": 0, "max": 5, "color": "#2b83ba" } ],
    "arrows": "/api/v1/jobs/01J.../files/arrows.geojson",
    "grid": "/api/v1/jobs/01J.../files/grid.json",
    "structures": "/api/v1/jobs/01J.../files/structures.geojson",
    "downloads": {
      "speed_geotiff": "/api/v1/jobs/01J.../files/speed.tif",
      "dir_geotiff": "/api/v1/jobs/01J.../files/dir.tif",
      "kmz": "/api/v1/jobs/01J.../files/result.kmz",
      "shapefile_zip": "/api/v1/jobs/01J.../files/shapefile.zip",
      "ascii_zip": "/api/v1/jobs/01J.../files/ascii.zip",
      "dem_geotiff": "/api/v1/jobs/01J.../files/dem.tif",
      "grid_csv": "/api/v1/jobs/01J.../files/grid.csv",
      "cfg": "/api/v1/jobs/01J.../files/job.cfg",
      "log": "/api/v1/jobs/01J.../log"
    },
    "stats": { "speed_min": 1.5, "speed_max": 22.3, "speed_mean": 9.8, "run_seconds": 27.4 }
  }
}

GET /api/v1/jobs/{id}/files/{name}

Returns the file with the correct Content-Type and Cache-Control: public, max-age=31536000, immutable. Nested paths are supported for per-timestep/per-height files, e.g. files/t00/speed.png or files/h30/speed.png.

GET /api/v1/jobs/{id}/log

text/plain
WindNinja_cli ...
Run 1
...

GET /api/v1/jobs/{id}/recipe

{
  "bbox": [-105.72, 39.54, -105.58, 39.64],
  "initialization": "domain_average",
  "...": "the exact params this job was created with",
  "_debug": { "dem_resolution_m": 100, "dem_cells": [629, 694] }
}

POST this object straight back to /api/v1/jobs to recreate the same run — unknown fields like _debug are ignored.

POST /api/v1/jobs/{id}/keepalive

204 No Content

Empty body. Bumps the job's "someone is still watching" timer so it isn't cleaned up while you're reading it.

GET /api/v1/dem/elevation?lat=&lon=

{ "elevation_m": 1823.4 }

GET /api/v1/geocode?q=

[
  { "label": "Mount Evans, Clear Creek County, Colorado, USA",
    "lat": 39.5883, "lon": -105.6438,
    "bbox": [-105.68, 39.56, -105.60, 39.62],
    "type": "peak", "importance": 0.52 }
]

GET /api/v1/geocode/reverse?lat=&lon=

{ "label": "Idaho Springs, Clear Creek County, Colorado, USA", "lat": 39.7428, "lon": -105.5172 }

GET /api/v1/weather/current?lat=&lon=

{
  "source": "open-meteo", "lat": 39.60, "lon": -105.65, "timezone": "America/Denver",
  "current": { "temperature_2m": 12.4, "wind_speed_10m": 9.1, "wind_direction_10m": 284,
    "wind_gusts_10m": 15.2, "relative_humidity_2m": 38, "cloud_cover": 20, "pressure_msl": 1016.2 },
  "hourly": { "...": "48h series: wind_speed_10m, wind_direction_10m, wind_gusts_10m, ..." },
  "fetched_at": "2026-10-06T14:00:00Z", "cache": "miss"
}

GET /api/v1/weather/stations?bbox=&sources=nws,metar,ndbc,raws

{
  "stations": [
    { "id": "KBJC", "name": "Rocky Mountain Metro Airport", "network": "nws",
      "lat": 39.909, "lon": -105.117, "elevation_m": 1725, "anemometer_height_m": 10,
      "wind_speed": 8.1, "wind_gust": 13.8, "wind_direction": 300, "temperature_c": 14.2,
      "observed_at": "2026-10-06T13:51:00Z", "source_url": "https://api.weather.gov/stations/KBJC" }
  ],
  "networks": {
    "nws": { "available": true, "count": 8 },
    "metar": { "available": true, "count": 4 },
    "ndbc": { "available": true, "count": 0 },
    "raws": { "available": false, "count": 0, "reason": "needs SYNOPTIC_TOKEN in /etc/windninja/windninja.env (free token at synopticdata.com)" }
  }
}

GET /api/v1/weather/profile?lat=&lon=&sources=model,sounding,surface

{
  "lat": 39.60, "lon": -105.65, "units": "mph", "fetched_at": "2026-10-06T14:00:00Z",
  "levels": [
    { "height_m": 10, "speed": 4.3, "direction": 327, "source": "model" },
    { "height_m": 10, "speed": 3.4, "direction": 220, "source": "surface", "station": "KCCU", "observed_at": "2026-10-06T13:51:00Z" },
    { "height_m": 436.5, "speed": 7.2, "direction": 46, "pressure_hpa": 600, "source": "model" }
  ],
  "sources_available": {
    "model": true, "sounding": true, "surface": true,
    "aircraft_amdar": { "available": false, "reason": "NOAA MADIS requires an account" },
    "satellite_winds": { "available": false, "reason": "assimilated into the model analysis; raw AMVs are not publicly served" }
  }
}

All endpoints are also reachable over a browser's normal fetch() — CORS is open and there is no API key to manage. Please keep automated request rates light; see Limits and fair use above.