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": [
    {
      "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-10-01T12:00:00Z",
          "validTimes": [
            "2026-10-01T12:00:00Z",
            "2026-10-01T13:00:00Z",
            "2026-10-01T14:00:00Z",
            "… 209 values, last 2026-10-17T12:00:00Z"
          ],
          "leadTimeHours": [
            0,
            1,
            2,
            "… 209 values, last 384"
          ],
          "links": {
            "canonical": "/v1/data/noaa-gfs-forecast/VFYKS8NF6KB1JMBQKB40/points/192,369?variables=temperature_2m&initTime=2026-10-01T12%3A00%3A00Z"
          },
          "data": {
            "temperature_2m": [
              17.75,
              17.875,
              18.125,
              "… 209 values, last 5.65625"
            ]
          }
        }
      ],
      "variables": {
        "temperature_2m": {
          "unit": "degree_Celsius"
        }
      },
      "snapshotId": "VFYKS8NF6KB1JMBQKB40",
      "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"
      }
    },
    "… 26 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": "VFYKS8NF6KB1JMBQKB40",
  "runs": [
    {
      "initTime": "2026-10-01T12:00:00Z"
    },
    {
      "initTime": "2026-10-01T06:00:00Z"
    },
    {
      "initTime": "2026-10-01T00: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-10-01T08:00:00Z","endTime":"2026-10-01T14:00:00Z","variables":["precipitation_surface"]}]}'
dynamical.org - API
Response
{
  "results": [
    {
      "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-10-01T08:00:00Z",
        "2026-10-01T09:00:00Z",
        "2026-10-01T10:00:00Z",
        "… 6 values, last 2026-10-01T13:00:00Z"
      ],
      "variables": {
        "precipitation_surface": {
          "unit": "kg m-2 s-1"
        }
      },
      "snapshotId": "V38H7VFWA1NFKRN9K0XG",
      "links": {
        "canonical": "/v1/data/noaa-mrms-conus-analysis-hourly/V38H7VFWA1NFKRN9K0XG/points/1312,4237?variables=precipitation_surface&startTime=2026-10-01T08%3A00%3A00Z&endTime=2026-10-01T14%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/VFYKS8NF6KB1JMBQKB40/points/192,369?variables=temperature_2m&initTime=2026-10-01T12%3A00%3A00Z'
dynamical.org - API
Response
{
  "dataProduct": {
    "id": "noaa-gfs-forecast",
    "version": "0.2.7"
  },
  "point": {
    "selected": {
      "latitude": 42,
      "longitude": -87.75,
      "grid": {
        "latitudeIndex": 192,
        "longitudeIndex": 369
      }
    },
    "method": "nearest"
  },
  "variables": {
    "temperature_2m": {
      "unit": "degree_Celsius"
    }
  },
  "snapshotId": "VFYKS8NF6KB1JMBQKB40",
  "forecasts": [
    {
      "initTime": "2026-10-01T12:00:00Z",
      "validTimes": [
        "2026-10-01T12:00:00Z",
        "2026-10-01T13:00:00Z",
        "2026-10-01T14:00:00Z",
        "… 209 values, last 2026-10-17T12:00:00Z"
      ],
      "leadTimeHours": [
        0,
        1,
        2,
        "… 209 values, last 384"
      ],
      "links": {
        "canonical": "/v1/data/noaa-gfs-forecast/VFYKS8NF6KB1JMBQKB40/points/192,369?variables=temperature_2m&initTime=2026-10-01T12%3A00%3A00Z"
      },
      "data": {
        "temperature_2m": [
          17.75,
          17.875,
          18.125,
          "… 209 values, last 5.65625"
        ]
      }
    }
  ],
  "links": {
    "catalog": "https://stac.dynamical.org/noaa-gfs-forecast/collection.json"
  }
}

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": [
    {
      "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-10-01T00:00:00Z",
          "validTimes": [
            "2026-10-01T00:00:00Z",
            "2026-10-01T03:00:00Z",
            "2026-10-01T06:00:00Z"
          ],
          "leadTimeHours": [
            0,
            3,
            6
          ],
          "links": {
            "canonical": "/v1/data/noaa-gefs-forecast-35-day/JQ3ZZE56R1S7CW14JC6G/points/192,369?variables=temperature_2m&initTime=2026-10-01T00%3A00%3A00Z&maxLeadTimeHours=6"
          },
          "ensembleMembers": [
            {
              "member": 0,
              "data": {
                "temperature_2m": [
                  17.75,
                  18.5,
                  18.5
                ]
              }
            },
            {
              "member": 1,
              "data": {
                "temperature_2m": [
                  17.5,
                  18.375,
                  18.625
                ]
              }
            },
            "… 31 entries"
          ]
        }
      ],
      "variables": {
        "temperature_2m": {
          "unit": "degree_Celsius"
        }
      },
      "snapshotId": "JQ3ZZE56R1S7CW14JC6G",
      "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

Even a small point response can require substantial data reads. Per-client request limits, response-size limits, and read-work limits keep the service predictable.

BoundLimit
Requests per client IP at the edge30 per 10 seconds, counted per Cloudflare location; exceeding this triggers a 10-second block
Queries per batch10
Runs resolved per forecast query10
Variables per query25
Span per analysis query31 days
Response values per batch100,000
Read values per request450,000,000 decoded chunk values across all queries and runs

The edge request limit is shared across POST /v1/forecasts, GET /v1/forecasts/runs, and POST /v1/analyses; browser OPTIONS preflights on those paths also count. Canonical point reads are outside that edge rule. The API's per-container rate limits remain an overload backstop. Contact [email protected] if you need higher limits.

The response-size budget counts values 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.

The separate read-value budget counts the full decoded source chunks needed for the request, including ensemble members. The whole batch is checked before any selected weather data is read; exceeding the budget returns 422 with the total and limit. Metadata and coordinate reads needed to select the data still occur. A small point response can require large chunks, especially for GRIB-backed virtual products, where a chunk is a whole grid.

Forecasts return all available lead times by default. Full-window requests to the three virtual GEFS forecast products exceed the read-value budget even for one variable. Use a shorter window with maxLeadTimeHours, or use the non-virtual noaa-gefs-forecast-35-day. Ensemble members multiply response values as well as read work; reduce variables, times, runs, or batch size to stay within both budgets.

Errors

API 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. Edge rate-limit errors are Cloudflare's error 1015, not this detail shape: HTML by default, or JSON when the request asks for it. Act on the status and Retry-After, not the body.

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
Batch total … exceeds the 450000000-value read-value budget (DYNAMICAL_API_MAX_READ_VALUES); reduce variables, times, runs, or batch size
429 Edge or API rate limit exceeded; honor Retry-After with either body Edge: Cloudflare error 1015 (HTML, or JSON if requested)
API: Request rate limit exceeded; contact [email protected] if you need higher limits
503 The API is at its concurrent-work limit (with Retry-After), or the upstream STAC catalog is unavailable Server busy; retry after N seconds
STAC catalog refresh failed (ReadTimeout)

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

Retries. On 429 or 503, wait at least as long as Retry-After specifies, then add random jitter. If the header is absent, use exponential backoff with jitter. Never retry immediately in a loop, and never retry a 422: change the request first.