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].
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.
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"]}]}'
{
"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.
dataProductId; a data-product resource calls its own identifier
id. Variable names are the underlying dataset's, unchanged
(temperature_2m).
Z on output. Timestamps you
send must carry a timezone offset; a naive timestamp is rejected rather than assumed to be
UTC.
initTime) from the value axis (validTimes, with
leadTimeHours alongside). There is no synthesized issue time. Analysis responses
have one axis, analysisTimes.
initTimesAfter and
initTimesBefore are both exclusive. Analysis startTime is inclusive
and endTime is exclusive. maxLeadTimeHours is an inclusive ceiling.
initTime, or sending
"initTime": "latest", selects the greatest initialization time present in the
opened snapshot. The response always reports the resolved timestamp — never the word
latest.
requested and selected
coordinates plus the grid indices. A location outside the grid is an error, not a snap to a
distant edge cell.
POST routes take a queries array and
return a results array in the same order. Attach an optional
queryId to a query and it is echoed on that result, so a batch stays unambiguous
without counting positions.
null, never
NaN — responses stay strict JSON.
X-Dynamical-Work-Units — to scripts, so a browser client can read everything a
command-line one can.
GET /v1/data-products
GET /v1/data-products/{dataProductId}
GET /v1/forecasts/runs
POST /v1/forecasts
POST /v1/analyses
GET /v1/data/{dataProductId}/{snapshotId}/points/{gridIndices}
/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.
curl 'https://api.dynamical.org/v1/data-products'
{
"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"
]
}
/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.
curl 'https://api.dynamical.org/v1/data-products/noaa-gfs-forecast'
{
"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"
}
}
/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.
| Parameter | Type | Notes |
|---|---|---|
dataProductId | string | Required; must be a forecast product |
initTimesAfter | timestamp | Exclusive lower bound |
initTimesBefore | timestamp | Exclusive upper bound |
limit | integer | 1–100, default 20 |
curl 'https://api.dynamical.org/v1/forecasts/runs?dataProductId=noaa-gfs-forecast&limit=3'
{
"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"
}
]
}
/v1/forecastsPoint 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.
| Field | Type | Notes |
|---|---|---|
dataProductId | string | Required; must be a forecast product |
location | object | Required; latitude −90..90, longitude −180..180 |
variables | string[] | Required; 1–25 names from the data product |
queryId | string | Echoed on the matching result; 1–128 characters |
initTime | timestamp or "latest" | Defaults to the latest run in the snapshot |
initTimesAfterinitTimesBefore | timestamp | A run range, both exclusive, sent together; mutually exclusive with initTime; at most 10 runs resolve |
validTimeStartvalidTimeEnd | timestamp | Trim the value axis; sent together, start exclusive of end |
maxLeadTimeHours | integer | Inclusive 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"]}]}'/v1/analyses
Point analysis time series — the observed or reanalyzed record rather than a forecast. One time
axis, analysisTimes, over a window you choose.
| Field | Type | Notes |
|---|---|---|
dataProductId | string | Required; must be an analysis product |
location | object | Required |
variables | string[] | Required; 1–25 names |
startTime | timestamp | Required; inclusive |
endTime | timestamp | Required; exclusive, after startTime, at most 31 days later |
queryId | string | Echoed on the matching result |
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"]}]}'
{
"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"
]
}
}
]
}
/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.
| Parameter | Where | Notes |
|---|---|---|
gridIndices | path | The two indices from point.selected.grid, comma-separated |
variables | query | Required; comma-separated, up to 25 |
initTime | query | Required for forecast products |
maxLeadTimeHoursvalidTimeStartvalidTimeEnd | query | Forecast value-axis trimming |
startTimeendTime | query | Analysis 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.
curl 'https://api.dynamical.org/v1/data/noaa-gfs-forecast/FHKS78ERQ44CTNM127G0/points/192,369?variables=temperature_2m&run=2026-08-05T12%3A00%3A00Z'
{
"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"
]
}
}
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.
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"]}]}'
{
"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"
}
}
]
}
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)
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];
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"]}]'
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.
| Bound | Limit |
|---|---|
| Queries per batch | 10 |
| Runs resolved per forecast query | 10 |
| Variables per query | 25 |
| Span per analysis query | 31 days |
| Response values per batch | 100,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 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.
| Status | When | Example detail |
|---|---|---|
| 404 | No such data product | Unknown data product 'nope' |
| 422 | The request cannot be answered as written | Location is outside the data product gridData product 'noaa-gfs-analysis' is analysis, not forecaststartTime 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.