API

OpenAPI (json)

Point access to the datasets in the catalog: forecast and analysis time series read straight from the Zarr/Icechunk archives on AWS Open Data. No account, no key, no credentials — including from a browser.

Base URL https://api.dynamical.org. Requests and responses are JSON in camelCase.

Every command below was run against the live API when this page was built, and the response under it is what came back — pretty-printed, and elided where an axis runs long, with the elision saying so in place. This is a beta: the /v1 shapes are what the service returns today, and breaking changes will arrive as a new path prefix. Tell us what you need at [email protected].

Quickstart

A point forecast is one request: a data product, a location, and the variables you want. That is the whole minimum — omit initTime and you get the most recent model run in the archive, with every lead time it publishes.

dynamical.org - API
Point forecast
curl -X POST 'https://api.dynamical.org/v1/forecasts' \
  -H 'content-type: application/json' \
  -d '{"queries":[{"dataProductId":"noaa-gfs-forecast","location":{"latitude":41.88,"longitude":-87.63},"variables":["temperature_2m"]}]}'
dynamical.org - API
Response
{
  "results": [
    {
      "query": {
        "dataProductId": "noaa-gfs-forecast",
        "location": {
          "latitude": 41.88,
          "longitude": -87.63
        },
        "variables": [
          "temperature_2m"
        ]
      },
      "dataProduct": {
        "id": "noaa-gfs-forecast",
        "version": "0.2.7"
      },
      "point": {
        "requested": {
          "latitude": 41.88,
          "longitude": -87.63
        },
        "selected": {
          "latitude": 42,
          "longitude": -87.75,
          "grid": {
            "latitudeIndex": 192,
            "longitudeIndex": 369
          }
        },
        "method": "nearest"
      },
      "forecasts": [
        {
          "initTime": "2026-08-05T12:00:00Z",
          "validTimes": [
            "2026-08-05T12:00:00Z",
            "2026-08-05T13:00:00Z",
            "2026-08-05T14:00:00Z",
            "… 209 values, last 2026-08-21T12:00:00Z"
          ],
          "leadTimeHours": [
            0,
            1,
            2,
            "… 209 values, last 384"
          ],
          "links": {
            "canonical": "/v1/data/noaa-gfs-forecast/FHKS78ERQ44CTNM127G0/points/192,369?variables=temperature_2m&run=2026-08-05T12%3A00%3A00Z"
          },
          "data": {
            "temperature_2m": [
              22.75,
              24.375,
              26.25,
              "… 209 values, last 26.125"
            ]
          }
        }
      ],
      "variables": {
        "temperature_2m": {
          "unit": "degree_Celsius"
        }
      },
      "snapshotId": "FHKS78ERQ44CTNM127G0",
      "links": {
        "catalog": "https://stac.dynamical.org/noaa-gfs-forecast/collection.json"
      }
    }
  ]
}

The response pairs each variable's values with the axes they sit on. validTimes and leadTimeHours are the value axis; initTime is the run the values came from; snapshotId is the exact Icechunk snapshot that was read. GFS publishes lead times out to 16 days, so that is what one variable returns here — add maxLeadTimeHours or a validTime window when you want less.

From there: list the data products to see what is available, list runs to pin an exact initialization time, or ask for analysis data to see what actually happened.

Conventions

Endpoints

GET /v1/data-products

Every data product the API can serve. The MVP catalog is the forecast and analysis products; capabilities says which point queries a product supports.

dynamical.org - API
Data products
curl 'https://api.dynamical.org/v1/data-products'
dynamical.org - API
Response
{
  "dataProducts": [
    {
      "id": "noaa-gfs-analysis",
      "name": "NOAA GFS analysis",
      "description": "Weather analysis from the Global Forecast System (GFS) operated by NOAA NWS NCEP.",
      "type": "analysis",
      "modelId": "noaa-gfs",
      "modelName": "NOAA GFS",
      "version": "0.1.0",
      "variableCount": 25,
      "capabilities": [
        "analysisTimeSeries"
      ],
      "links": {
        "self": "/v1/data-products/noaa-gfs-analysis",
        "catalog": "https://stac.dynamical.org/noaa-gfs-analysis/collection.json"
      }
    },
    {
      "id": "noaa-gfs-forecast",
      "name": "NOAA GFS forecast",
      "description": "Weather forecasts from the Global Forecast System (GFS) operated by NOAA NWS NCEP.",
      "type": "forecast",
      "modelId": "noaa-gfs",
      "modelName": "NOAA GFS",
      "version": "0.2.7",
      "variableCount": 25,
      "capabilities": [
        "forecastTimeSeries"
      ],
      "links": {
        "self": "/v1/data-products/noaa-gfs-forecast",
        "catalog": "https://stac.dynamical.org/noaa-gfs-forecast/collection.json"
      }
    },
    "… 15 entries"
  ]
}

GET /v1/data-products/{dataProductId}

The summary above plus what you need to build a query: every variable with its unit and dimensions, the dimension extents, the bounding box, and the license. An open-ended extent upper bound (null) means the archive is still growing along that dimension.

dynamical.org - API
One data product
curl 'https://api.dynamical.org/v1/data-products/noaa-gfs-forecast'
dynamical.org - API
Response
{
  "id": "noaa-gfs-forecast",
  "name": "NOAA GFS forecast",
  "description": "Weather forecasts from the Global Forecast System (GFS) operated by NOAA NWS NCEP.",
  "type": "forecast",
  "modelId": "noaa-gfs",
  "modelName": "NOAA GFS",
  "version": "0.2.7",
  "variableCount": 25,
  "capabilities": [
    "forecastTimeSeries"
  ],
  "links": {
    "self": "/v1/data-products/noaa-gfs-forecast",
    "catalog": "https://stac.dynamical.org/noaa-gfs-forecast/collection.json"
  },
  "license": "CC-BY-4.0",
  "boundingBox": [
    -180,
    -90,
    179.75,
    "… 4 values, last 90"
  ],
  "dimensions": {
    "init_time": {
      "type": "temporal",
      "extent": [
        "2021-05-01T00:00:00Z",
        null
      ],
      "unit": "seconds since 1970-01-01"
    },
    "latitude": {
      "type": "spatial",
      "extent": [
        -90,
        90
      ],
      "axis": "y",
      "unit": "degree_north",
      "size": 721
    },
    "lead_time": {
      "type": "other",
      "extent": [
        0,
        1382400
      ],
      "unit": "seconds",
      "size": 209
    },
    "longitude": {
      "type": "spatial",
      "extent": [
        -180,
        179.75
      ],
      "axis": "x",
      "unit": "degree_east",
      "size": 1440
    }
  },
  "variables": {
    "categorical_freezing_rain_surface": {
      "dimensions": [
        "init_time",
        "lead_time",
        "latitude",
        "… 4 values, last longitude"
      ],
      "unit": "1",
      "longName": "Categorical freezing rain",
      "standardName": null
    },
    "…": "24 more"
  }
}

GET /v1/forecasts/runs

The initialization times actually present in the current snapshot, newest first, with the snapshotId they were read from. It reports what exists; it does not infer whether a run is pending or complete.

ParameterTypeNotes
dataProductIdstringRequired; must be a forecast product
initTimesAftertimestampExclusive lower bound
initTimesBeforetimestampExclusive upper bound
limitinteger1–100, default 20
dynamical.org - API
Forecast runs
curl 'https://api.dynamical.org/v1/forecasts/runs?dataProductId=noaa-gfs-forecast&limit=3'
dynamical.org - API
Response
{
  "dataProductId": "noaa-gfs-forecast",
  "snapshotId": "FHKS78ERQ44CTNM127G0",
  "runs": [
    {
      "initTime": "2026-08-05T12:00:00Z"
    },
    {
      "initTime": "2026-08-05T06:00:00Z"
    },
    {
      "initTime": "2026-08-05T00:00:00Z"
    }
  ]
}

POST /v1/forecasts

Point forecast time series, up to 10 queries per request. Each query selects one location, one data product, and one or more runs. The quickstart is the minimal form; everything else here is optional.

FieldTypeNotes
dataProductIdstringRequired; must be a forecast product
locationobjectRequired; latitude −90..90, longitude −180..180
variablesstring[]Required; 1–25 names from the data product
queryIdstringEchoed on the matching result; 1–128 characters
initTimetimestamp or "latest"Defaults to the latest run in the snapshot
initTimesAfter
initTimesBefore
timestampA run range, both exclusive, sent together; mutually exclusive with initTime; at most 10 runs resolve
validTimeStart
validTimeEnd
timestampTrim the value axis; sent together, start exclusive of end
maxLeadTimeHoursintegerInclusive ceiling on leadTimeHours; omit for every lead time the product publishes

A run range instead of a single run returns one entry per resolved run in forecasts — that is how you compare successive runs at a point. Each entry carries its own canonical link.

curl -X POST 'https://api.dynamical.org/v1/forecasts' \
  -H 'content-type: application/json' \
  -d '{"queries":[{"dataProductId":"noaa-gfs-forecast",
                   "location":{"latitude":41.88,"longitude":-87.63},
                   "initTimesAfter":"2026-08-04T00:00:00Z",
                   "initTimesBefore":"2026-08-05T18:00:00Z",
                   "maxLeadTimeHours":6,
                   "variables":["temperature_2m"]}]}'

POST /v1/analyses

Point analysis time series — the observed or reanalyzed record rather than a forecast. One time axis, analysisTimes, over a window you choose.

FieldTypeNotes
dataProductIdstringRequired; must be an analysis product
locationobjectRequired
variablesstring[]Required; 1–25 names
startTimetimestampRequired; inclusive
endTimetimestampRequired; exclusive, after startTime, at most 31 days later
queryIdstringEchoed on the matching result
dynamical.org - API
Hourly analysis
curl -X POST 'https://api.dynamical.org/v1/analyses' \
  -H 'content-type: application/json' \
  -d '{"queries":[{"dataProductId":"noaa-mrms-conus-analysis-hourly","location":{"latitude":41.88,"longitude":-87.63},"startTime":"2026-08-05T10:00:00Z","endTime":"2026-08-05T16:00:00Z","variables":["precipitation_surface"]}]}'
dynamical.org - API
Response
{
  "results": [
    {
      "query": {
        "dataProductId": "noaa-mrms-conus-analysis-hourly",
        "location": {
          "latitude": 41.88,
          "longitude": -87.63
        },
        "variables": [
          "precipitation_surface"
        ],
        "startTime": "2026-08-05T10:00:00Z",
        "endTime": "2026-08-05T16:00:00Z"
      },
      "dataProduct": {
        "id": "noaa-mrms-conus-analysis-hourly",
        "version": "0.3.0"
      },
      "point": {
        "requested": {
          "latitude": 41.88,
          "longitude": -87.63
        },
        "selected": {
          "latitude": 41.87500000000261,
          "longitude": -87.62500000003854,
          "grid": {
            "latitudeIndex": 1312,
            "longitudeIndex": 4237
          }
        },
        "method": "nearest"
      },
      "analysisTimes": [
        "2026-08-05T10:00:00Z",
        "2026-08-05T11:00:00Z",
        "2026-08-05T12:00:00Z",
        "… 6 values, last 2026-08-05T15:00:00Z"
      ],
      "variables": {
        "precipitation_surface": {
          "unit": "kg m-2 s-1"
        }
      },
      "snapshotId": "TT22QP0G893NV57YADWG",
      "links": {
        "canonical": "/v1/data/noaa-mrms-conus-analysis-hourly/TT22QP0G893NV57YADWG/points/1312,4237?variables=precipitation_surface&start=2026-08-05T10%3A00%3A00Z&end=2026-08-05T16%3A00%3A00Z",
        "catalog": "https://stac.dynamical.org/noaa-mrms-conus-analysis-hourly/collection.json"
      },
      "data": {
        "precipitation_surface": [
          0,
          0,
          0,
          "… 6 values, last 0"
        ]
      }
    }
  ]
}

GET /v1/data/{dataProductId}/{snapshotId}/points/{gridIndices}

The immutable form of a point response. Every POST result hands you one of these URLs on links.canonical: the resolved snapshot, the resolved grid indices, and the exact query parameters that produced the values. Because the snapshot and the cell are pinned, the body can never change — it is served Cache-Control: public, max-age=31536000, immutable and is the right URL to hand a CDN, a notebook, or a citation.

ParameterWhereNotes
gridIndicespathThe two indices from point.selected.grid, comma-separated
variablesqueryRequired; comma-separated, up to 25
initTimequeryRequired for forecast products
maxLeadTimeHours
validTimeStart
validTimeEnd
queryForecast value-axis trimming
startTime
endTime
queryAnalysis window

The command below is the canonical link the quickstart's response returned, followed on this page's last build. Snapshots roll forward as new data lands, so this exact URL stops resolving once the archive moves past it — read the link out of your own response rather than copying one.

dynamical.org - API
Canonical point
curl 'https://api.dynamical.org/v1/data/noaa-gfs-forecast/FHKS78ERQ44CTNM127G0/points/192,369?variables=temperature_2m&run=2026-08-05T12%3A00%3A00Z'
dynamical.org - API
Response
{
  "source": {
    "collection": "noaa-gfs-forecast",
    "dataset_version": "0.2.7",
    "snapshot": "FHKS78ERQ44CTNM127G0",
    "run": "2026-08-05T12:00:00Z",
    "run_selection": "explicit"
  },
  "point": {
    "latitude": 42,
    "longitude": -87.75,
    "grid": {
      "latitude_index": 192,
      "longitude_index": 369
    },
    "method": "nearest"
  },
  "time": [
    "2026-08-05T12:00:00Z",
    "2026-08-05T13:00:00Z",
    "2026-08-05T14:00:00Z",
    "… 209 values, last 2026-08-21T12:00:00Z"
  ],
  "lead_time_hours": [
    0,
    1,
    2,
    "… 209 values, last 384"
  ],
  "units": {
    "temperature_2m": "degree_Celsius"
  },
  "links": {
    "canonical": "/v1/data/noaa-gfs-forecast/FHKS78ERQ44CTNM127G0/points/192,369?variables=temperature_2m&run=2026-08-05T12%3A00%3A00Z",
    "catalog": "https://stac.dynamical.org/noaa-gfs-forecast/collection.json"
  },
  "data": {
    "temperature_2m": [
      22.75,
      24.375,
      26.25,
      "… 209 values, last 26.125"
    ]
  }
}

Ensembles

Products with an ensemble_member dimension return every member of the source archive, in order. The response replaces data with ensembleMembers; each entry keeps the archive's own member identifier and a data object with the same variable series a deterministic response would carry. Axes, units, and links are unchanged, so member 0 is read exactly like a deterministic forecast.

Members multiply the response: they count against the work budget below exactly as extra variables or lead times do. That is why this example trims the value axis — 31 members across a 35-day horizon is a large answer to ask for by accident.

dynamical.org - API
Ensemble forecast
curl -X POST 'https://api.dynamical.org/v1/forecasts' \
  -H 'content-type: application/json' \
  -d '{"queries":[{"dataProductId":"noaa-gefs-forecast-35-day","location":{"latitude":41.88,"longitude":-87.63},"maxLeadTimeHours":6,"variables":["temperature_2m"]}]}'
dynamical.org - API
Response
{
  "results": [
    {
      "query": {
        "dataProductId": "noaa-gefs-forecast-35-day",
        "location": {
          "latitude": 41.88,
          "longitude": -87.63
        },
        "variables": [
          "temperature_2m"
        ],
        "maxLeadTimeHours": 6
      },
      "dataProduct": {
        "id": "noaa-gefs-forecast-35-day",
        "version": "0.2.0"
      },
      "point": {
        "requested": {
          "latitude": 41.88,
          "longitude": -87.63
        },
        "selected": {
          "latitude": 42,
          "longitude": -87.75,
          "grid": {
            "latitudeIndex": 192,
            "longitudeIndex": 369
          }
        },
        "method": "nearest"
      },
      "forecasts": [
        {
          "initTime": "2026-08-05T00:00:00Z",
          "validTimes": [
            "2026-08-05T00:00:00Z",
            "2026-08-05T03:00:00Z",
            "2026-08-05T06:00:00Z"
          ],
          "leadTimeHours": [
            0,
            3,
            6
          ],
          "links": {
            "canonical": "/v1/data/noaa-gefs-forecast-35-day/9WPBYC65D777E9WQB0JG/points/192,369?variables=temperature_2m&run=2026-08-05T00%3A00%3A00Z&max_lead_time_hours=6"
          },
          "ensembleMembers": [
            {
              "member": 0,
              "data": {
                "temperature_2m": [
                  27.25,
                  25,
                  23.75
                ]
              }
            },
            {
              "member": 1,
              "data": {
                "temperature_2m": [
                  27.125,
                  25.125,
                  23.5
                ]
              }
            },
            "… 31 entries"
          ]
        }
      ],
      "variables": {
        "temperature_2m": {
          "unit": "degree_Celsius"
        }
      },
      "snapshotId": "9WPBYC65D777E9WQB0JG",
      "links": {
        "catalog": "https://stac.dynamical.org/noaa-gefs-forecast-35-day/collection.json"
      }
    }
  ]
}

Clients

dynamical.org - API
Python
import httpx

response = httpx.post(
    "https://api.dynamical.org/v1/forecasts",
    json={
        "queries": [
            {
                "dataProductId": "noaa-gfs-forecast",
                "location": {"latitude": 41.88, "longitude": -87.63},
                "variables": ["temperature_2m"],
            }
        ]
    },
    timeout=60,
)
response.raise_for_status()
forecast = response.json()["results"][0]["forecasts"][0]
for valid_time, value in zip(forecast["validTimes"], forecast["data"]["temperature_2m"]):
    print(valid_time, value)
dynamical.org - API
JavaScript
const response = await fetch("https://api.dynamical.org/v1/forecasts", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({
    queries: [
      {
        dataProductId: "noaa-gfs-forecast",
        location: { latitude: 41.88, longitude: -87.63 },
        variables: ["temperature_2m"],
      },
    ],
  }),
});
const { results } = await response.json();
const { validTimes, data } = results[0].forecasts[0];
dynamical.org - API
HTTPie
http GET api.dynamical.org/v1/forecasts/runs dataProductId==noaa-gfs-forecast limit==1

http POST api.dynamical.org/v1/analyses \
  queries:='[{"dataProductId":"noaa-mrms-conus-analysis-hourly",
              "location":{"latitude":41.88,"longitude":-87.63},
              "startTime":"2026-08-04T00:00:00Z",
              "endTime":"2026-08-04T06:00:00Z",
              "variables":["precipitation_surface"]}]'

Bounds and work units

A point request is cheap, but a careless one can ask for millions of values. These are the limits that keep the service predictable rather than a per-caller quota.

BoundLimit
Queries per batch10
Runs resolved per forecast query10
Variables per query25
Span per analysis query31 days
Response values per batch100,000

The work budget is the real size bound: values are counted across variables, times, runs, and ensemble members, and a batch that would exceed 100,000 is rejected rather than truncated. A successful response reports what it actually used in X-Dynamical-Work-Units — measure a small version of your query, then scale.

Errors

Errors are a JSON object with a detail string, or — for request-shape failures — FastAPI's validation array under the same key. Nothing is silently coerced: an unknown field, a naive timestamp, or a location off the grid is an error, not a guess.

StatusWhenExample detail
404 No such data product Unknown data product 'nope'
422 The request cannot be answered as written Location is outside the data product grid
Data product 'noaa-gfs-analysis' is analysis, not forecast
startTime must include a timezone
429 Rate limit exceeded; honor Retry-After Request rate limit exceeded
503 The upstream STAC catalog is unavailable Failed to load STAC catalog …

A batch is all-or-nothing: if one query in queries fails, the whole request fails, so a 200 means every result is present.