Weather Portal

Purpose and endpoint

POST /api/weather/query queries weather for one geographic point. It returns numerical fields, features and messages, daily forecasts, derived diagnostics or availability metadata. The URL is unversioned; schema_version identifies the contract.

On the portal use the relative path /api/weather/query. The portal forwards the request to Weather Data Hub, which selects the model publication and reads the data. One request may contain multiple named queries. A quick current-weather card, a longer forecast and features can load separately using the same contract.

The API accepts coordinates, not ICAO identifiers. For an airport widget resolve the airport by ICAO first, then pass its coordinates here. Exact-ICAO METAR/TAF retrieval remains a separate airport-report request; nearby feature search does not replace an exact ICAO lookup.

Quick start

Minimal request for temperature at 2 m and surface pressure. Omitting time uses the current time with valid_at selection. This is a read operation: POST does not create a calculation, scenario or record.

Request body
{
  "schema_version": "weather-query-1",
  "point": {
    "lat": 55.41,
    "lon": 37.91
  },
  "queries": [
    {
      "id": "current",
      "kind": "samples",
      "source": {
        "model": "noaa"
      },
      "fields": [
        {
          "id": "temperature_2m",
          "parameter": "air_temperature",
          "level": {
            "type": "height_agl",
            "value": 2,
            "unit": "m"
          }
        },
        {
          "id": "surface_pressure",
          "parameter": "surface_air_pressure",
          "level": {
            "type": "surface"
          }
        }
      ]
    }
  ]
}
curl: replace the portal origin
curl --fail-with-body 'https://YOUR_PORTAL/api/weather/query' \
  -H 'Content-Type: application/json' \
  --data-binary @request.json
JavaScript: current weather without a global sdk variable
const request = {
  "schema_version": "weather-query-1",
  "point": {
    "lat": 55.41,
    "lon": 37.91
  },
  "queries": [
    {
      "id": "current",
      "kind": "samples",
      "source": {
        "model": "noaa"
      },
      "fields": [
        {
          "id": "temperature_2m",
          "parameter": "air_temperature",
          "level": {
            "type": "height_agl",
            "value": 2,
            "unit": "m"
          }
        },
        {
          "id": "surface_pressure",
          "parameter": "surface_air_pressure",
          "level": {
            "type": "surface"
          }
        }
      ]
    }
  ]
};
const response = await fetch('/api/weather/query', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  credentials: 'omit',
  body: JSON.stringify(request)
});
const payload = await response.json();
if (!response.ok) throw new Error(JSON.stringify(payload));
const current = payload.results.find(result => result.id === 'current');
const definitions = new Map(current.fields.map(field => [field.id, field]));
for (const sample of current.samples) {
  for (const value of sample.values) {
    const field = definitions.get(value.field_id);
    console.log(sample.valid_time, field.parameter, value.value, field.unit, value.status);
  }
}

Request structure

Top-level fields
FieldRule
schema_versionRequired: weather-query-1.
pointObject {lat, lon}; required unless all queries are availability. Latitude −90…90, longitude −180…180; WGS84 degrees.
queries1–16 queries; each id is unique within the batch and at most 64 characters. Match results by id.
queries[].kindsamples | features | daily | diagnostics | availability
queries[].sourceModel data: {model, member?, cid?}. NOAA is the default. cid selects a CKS centre (integer 0–65535), requires model:cks or CKS layers, filters CKS only and is part of publication identity. Features select collection and sources instead.
queries[].runDefault {policy:"latest"}; exact run: {policy:"exact", at:"…Z"}. publication_token pins a publication.
queries[].timeAn at instant or a from/to interval with grid:"native". See time selection below.

Send application/json. Coordinates must be finite numbers. Arbitrary SQL expressions, table names and column names are not accepted; the server controls source and parameter selection.

Query IDs and explicit field IDs use 1–64 characters from A–Z, a–z, 0–9, _, ., :, -. Unknown request keys are rejected. For daily and features, time supports at only; availability does not accept time. Diagnostics supports noaa, roshydro or gefs.

Sources, parameters and levels

Model sources
modelPurpose
noaaNOAA deterministic fields.
roshydroRoshydromet fields.
gefsGEFS ensemble fields; supported member: "geavg".
cksAdditional CKS fields; ref_time may be absent.
noaa_turbulenceAdditional composite turbulence fields.

Parameter availability depends on published source data. For fields:"all" and native:… parameters the server preserves provider names and units with normalized:false. Do not infer temperature or pressure semantics from an unfamiliar field name.

Canonical parameters
parameterNative parameterResponse unit
air_temperatureTMPdegC
relative_humidityRH%
surface_air_pressurePREShPa
air_pressure_at_mean_sea_levelPRMSLhPa
cloud_area_fractionTCDC%
precipitation_ratePRATEmm/h
precipitation_amountAPCPmm
eastward_windUGRDm/s
northward_windVGRDm/s
wind_speedWIND_SPEEDm/s
wind_from_directionWIND_DIRECTIONdegree_from_true_north
wind_speed_of_gustGUSTm/s
geopotential_heightHGTm
Typed levels
levelMeaning
{"type":"surface"}Model surface.
{"type":"height_agl","value":2,"unit":"m"}Height above ground level.
{"type":"pressure","value":850,"unit":"hPa"}Isobaric level; not height above the ground.
{"type":"mean_sea_level"}Mean sea level.
{"type":"entire_atmosphere"}Entire atmosphere; for example total cloud cover.
{"type":"native","value":"850hPa"}Exact provider level identifier.

Surface PRES is model surface pressure; it is neither observed METAR QNH nor PRMSL. Total TCDC uses entire_atmosphere. Wind direction indicates where the wind comes FROM relative to true north. APCP is accumulated precipitation: check accumulation intervals; do not blindly sum it as independent hourly amounts.

Numerical samples and time series

kind:"samples" accepts one selector: explicit fields with parameters and levels, fields:"all" for provider fields, or layers identifying published layers. Do not combine selectors within one query. Explicit fields limits reads to the required quantities.

Airport temperature and pressure forecast
{
  "schema_version": "weather-query-1",
  "point": {
    "lat": 55.41,
    "lon": 37.91
  },
  "queries": [
    {
      "id": "airport_forecast",
      "kind": "samples",
      "source": {
        "model": "noaa"
      },
      "time": {
        "from": "2026-10-20T06:00:00Z",
        "to": "2026-10-23T06:00:00Z",
        "grid": "native"
      },
      "fields": [
        {
          "id": "temperature_2m",
          "parameter": "air_temperature",
          "level": {
            "type": "height_agl",
            "value": 2,
            "unit": "m"
          }
        },
        {
          "id": "surface_pressure",
          "parameter": "surface_air_pressure",
          "level": {
            "type": "surface"
          }
        }
      ]
    }
  ]
}

Example dates are illustrative: replace them with times reported by availability. If to is omitted, published future times from from are returned within the limit. The API does not manufacture hourly steps or interpolate between native forecast times.

Selecting map layers
{
  "schema_version": "weather-query-1",
  "point": {
    "lat": 55.41,
    "lon": 37.91
  },
  "queries": [
    {
      "id": "map_fields",
      "kind": "samples",
      "layers": [
        "image_tmp_noaa",
        "image_pres_noaa",
        "image_tcdc_noaa"
      ]
    }
  ]
}

Without a shared level, TMP and RH layers use 2 m, PRES uses surface, PRMSL mean_sea_level, TCDC entire_atmosphere, and other layers surface. For a height-specific request set level explicitly and inspect fields[].level. Layers from different models receive separate publication records.

Temperature at a pressure level
{
  "schema_version": "weather-query-1",
  "point": {
    "lat": 55.41,
    "lon": 37.91
  },
  "queries": [
    {
      "id": "temperature_850",
      "kind": "samples",
      "source": {
        "model": "noaa"
      },
      "fields": [
        {
          "id": "t850",
          "parameter": "air_temperature",
          "level": {
            "type": "pressure",
            "value": 850,
            "unit": "hPa"
          }
        }
      ]
    }
  ]
}

Time and model-run selection

Time policies
timeBehavior
{"at":"…Z","selection":"valid_at"}Selects a field valid at the requested instant; the standard source validity window is 3 hours. Returns the actual field time.
{"at":"…Z","selection":"exact"}Only the exact native time. A missing time is not replaced by a neighboring time.
{"at":"…Z","selection":"nearest","max_offset_seconds":5400,"tie_break":"earlier"}Nearest time within the explicit tolerance. max_offset_seconds and tie_break are required; tie_break is earlier or later.
{"from":"…Z","to":"…Z","grid":"native"}Published native times in the interval.

All input timestamps must include Z or a timezone offset. Model timestamps in responses use UTC. Requested time, model run_time, forecast valid_time and value validity valid_from/valid_to have different meanings. Do not label data with requested time when the actual forecast time differs.

run.policy:"latest" selects one latest model run and does not fill its gaps from older runs. Use run:{policy:"exact",at:"…Z"} for reproducibility. CKS run time may be unknown: null does not mean now.

valid_at/exact/nearest selection and native intervals apply to samples and diagnostics. nearest tolerance is 0–86400 seconds. Daily selects local dates relative to time.at; features selects features valid at time.at.

Consistency and publication_token

publications contains the publication ID, source, run time and opaque token. fields[].publication_id links each field to its publication; multiple sources do not imply a common run. Numerical batch reads use a consistent database snapshot. Observation messages have their own timestamps and are not part of a forecast model run.

To keep a separately loaded forecast consistent with the current-weather card, pass that publication token as run.publication_token in the next query for the same source. Do not compute tokens yourself or replace them with run time. Republishing data can change the token while run_time remains the same.

Pinning a publication
{
  "id": "forecast",
  "kind": "samples",
  "source": {
    "model": "noaa"
  },
  "run": {
    "policy": "latest",
    "publication_token": "TOKEN_FROM_PUBLICATIONS"
  },
  "time": {
    "from": "2026-10-20T06:00:00Z",
    "grid": "native"
  },
  "fields": [
    {
      "id": "temperature_2m",
      "parameter": "air_temperature",
      "level": {
        "type": "height_agl",
        "value": 2,
        "unit": "m"
      }
    },
    {
      "id": "surface_pressure",
      "parameter": "surface_air_pressure",
      "level": {
        "type": "surface"
      }
    }
  ]
}

HTTP 409 publication_changed means the pinned publication no longer matches. Clear related old-card data, reload the card without the stale token, then request the forecast with the new token. Bound retries and do not display a mixture of old and new publications.

For layers spanning multiple models pass run.publication_tokens as {publication_id: token}. Copy id and token from publications unchanged. A batch cannot request different runs of the same source: this is a 422 mixed_runs error.

Features, zones and messages

kind:"features" returns a GeoJSON FeatureCollection. collection:"vectors" selects weather features, observations and messages; collection:"opmet" selects OPMET conditions covering the point. sources selects vector sources; omitting it uses the server set. Radius is in meters.

Nearby observations and covering OPMET conditions
{
  "schema_version": "weather-query-1",
  "point": {
    "lat": 55.41,
    "lon": 37.91
  },
  "queries": [
    {
      "id": "nearby_reports",
      "kind": "features",
      "collection": "vectors",
      "sources": [
        "metars_noaa",
        "tafs_noaa"
      ],
      "spatial": {
        "relation": "within_radius",
        "radius_m": 50000
      },
      "page": {
        "limit": 20,
        "offset": 0
      }
    },
    {
      "id": "covering_opmet",
      "kind": "features",
      "collection": "opmet",
      "spatial": {
        "relation": "covers_point"
      }
    }
  ]
}

Vector sources: clouds, turbulence, icing_noaa, clouds_noaa, turbulence_noaa, jet_stream, jet_stream_points, jet_stream_noaa, jet_stream_points_noaa, tropopause, tropopause_noaa, fronts, fronts_noaa, front_points, front_points_noaa, volcano, volcano_noaa, volcanic_ash_clouds, volcanic_ash_volcanoes, cyclone_forecast_points, cyclone_forecast_tracks, active_tropical_cyclones_history, recent_tropical_cyclones, aircraft_reports_noaa, metars, metars_noaa, tafs, tafs_noaa, tsa. Actual coverage depends on ingested data.

Response sources[] reports per-source status, coverage_verified and pagination. Request a next page for the specific source using its offset; a common limit is not a total limit for the merged collection. Provider attributes are retained in properties.source_properties. Inspect time validity, relation, distance_m and vertical datum; AGL, MSL and flight levels are not interchangeable.

Vectors supports within_radius; OPMET supports covers_point. An empty collection with coverage_verified:false means coverage is unknown, not that hazardous phenomena are proven absent. stale, unavailable and empty are different states.

Default vector sources omit aircraft_reports_noaa; this source depends on a server feature flag. Request only necessary sources: each has independent pagination limits and coverage.

Daily forecast and local dates

Daily forecast for selected dates
{
  "schema_version": "weather-query-1",
  "point": {
    "lat": 55.41,
    "lon": 37.91
  },
  "queries": [
    {
      "id": "daily",
      "kind": "daily",
      "source": {
        "model": "noaa"
      },
      "preset": "near_surface_weather",
      "timezone": "Europe/Moscow",
      "lang": "ru",
      "dates": [
        "2026-10-20",
        "2026-10-21"
      ]
    }
  ]
}

preset:"near_surface_weather" explicitly selects the full near-surface field set for deterministic descriptions. timezone is an IANA name, such as Europe/Moscow or UTC; lang is ru or en. dates accepts 1–64 unique local YYYY-MM-DD dates. Omitting dates selects the available future horizon.

Numerical data is stored in common fields and samples. days[].sample_times references samples[].valid_time; current.sample_time points to the current sample. days includes daily summaries, periods and text; additional metadata includes timezone, language, algorithm_version, native_steps_hours, quality, notes and available dates. Descriptions are deterministic, without AI inference.

Minima and maxima are calculated from available native samples, not from a continuous temperature curve. Quality metadata reports gaps and missing required fields. For a temperature and pressure chart use samples with two fields; the full daily preset is unnecessary.

Vertical column and diagnostics

Column fields and derived diagnostics
{
  "schema_version": "weather-query-1",
  "point": {
    "lat": 55.41,
    "lon": 37.91
  },
  "queries": [
    {
      "id": "column",
      "kind": "diagnostics",
      "source": {
        "model": "noaa"
      },
      "time": {
        "at": "2026-10-20T06:00:00Z",
        "selection": "exact"
      }
    }
  ]
}

diagnostics uses the same numerical reads as fields:"all" and adds diagnostics, profile and algorithm_version:"point-column-1". profile is a derived presentation; original values remain in fields/samples. Isotherm heights and wind shear require the corresponding input fields. Missing input is not replaced by zero.

For additional columns submit a separate samples query with fields:"all" and source.model:"cks" or "noaa_turbulence". These fields may retain provider units and level identifiers; inspect normalized.

Available times and dates

Catalog without coordinates or point raster sampling
{
  "schema_version": "weather-query-1",
  "queries": [
    {
      "id": "dates",
      "kind": "availability",
      "source": {
        "model": "noaa"
      },
      "timezone": "Europe/Moscow"
    }
  ]
}

The result includes metadata_only:true, point_coverage_verified:false, available_dates, forecast_times, fields and a publication reference. This is a lightweight publication catalog: it does not run ST_Value or test values at a point. A catalog date does not guarantee full coverage for every coordinate or parameter.

Load the catalog independently of the numerical card. Once a date is selected, submit samples or daily; pin the returned publication token when consistency is required.

Common response and missing data

Short illustrative response; values and tokens are fictional
{
  "schema_version": "weather-query-1",
  "request_id": "example-request-id",
  "status": "complete",
  "point": {
    "lat": 55.41,
    "lon": 37.91
  },
  "publications": [
    {
      "id": "0123456789abcdef",
      "source": {
        "model": "noaa"
      },
      "run_time": "2026-10-20T00:00:00Z",
      "token": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
      "consistency": "model_run_snapshot"
    }
  ],
  "results": [
    {
      "id": "current",
      "kind": "samples",
      "status": "complete",
      "source": {
        "model": "noaa"
      },
      "publication_id": "0123456789abcdef",
      "fields": [
        {
          "id": "temperature_2m",
          "parameter": "air_temperature",
          "native_parameter": "TMP",
          "native_level": "2 m above ground",
          "level": {
            "type": "height_agl",
            "value": 2,
            "unit": "m"
          },
          "unit": "degC",
          "normalized": true,
          "statistic": "unspecified",
          "origin": "model",
          "publication_id": "0123456789abcdef"
        }
      ],
      "samples": [
        {
          "valid_time": "2026-10-20T06:00:00Z",
          "values": [
            {
              "field_id": "temperature_2m",
              "value": 7.5,
              "status": "available",
              "quality": [],
              "source_unit": "K",
              "valid_from": "2026-10-20T06:00:00Z",
              "valid_to": "2026-10-20T09:00:00Z",
              "sampling": "bilinear"
            }
          ]
        }
      ],
      "issues": []
    }
  ]
}

Top-level and query status can be complete, partial, unavailable or empty. HTTP 200 does not guarantee every value is present: inspect result.status, issues and each value.status. Value ordering is not a contract; join definitions through field_id. Units belong to field definitions; model provenance is recorded in origin and publication_id.

Numerical value states
statusMeaning
availableA value is available; zero is a valid value.
nodataThe raster field exists but the point has no value.
missingA required field, level or time is missing.
conflictConflicting source rows prevent an unambiguous value.
unsupported_unitThe unit cannot be safely converted to the canonical unit.

Missing values use value:null. Do not convert null to zero or draw across gaps without an explicit UI policy. sampling and quality describe sampling and quality limitations. source_unit retains the provider unit. For unknown native fields normalized:false means that canonical physical semantics are not guaranteed.

statistic:"unspecified" means source metadata does not establish the statistical meaning. APCP statistic:"accumulation" with quality:["interval_unknown"] does not identify accumulation boundaries: valid_from/valid_to describe field validity, not the precipitation accumulation interval. sampling:"derived_from_bilinear_components" identifies wind derived from normalized U/V components.

Errors, limits and retries

Validation error example
{
  "schema_version": "weather-query-1",
  "request_id": "example-request-id",
  "status": "error",
  "error": {
    "code": "invalid_query",
    "message": "point is required"
  }
}
HTTP status codes
CodeMeaning and action
200Response returned; partial/empty results and issues may be present.
400Malformed JSON. Correct the request body.
413Request body exceeds 64 KiB. Split the request.
415The portal requires Content-Type: application/json.
422Invalid coordinates, fields, levels, times or query cost.
409publication_changed: reload related data with a new publication.
502The portal could not obtain a valid bounded Hub response.
503All requested data is unavailable. A bounded retry may be appropriate.
504The deadline expired. Reduce the request or retry later.
Limits
ResourceMaximum
Request body65,536 bytes
Response8 MiB
Queries per batch16
Explicit fields per query64
Fields with fields:"all"3,000
Native times per query512
Sampled rows per batch16,000
Explicit local dates64
Features per source per page50
Radius250,000 m
offset10,000
Hub deadline / portal timeout45 s / 60 s

When a budget is exceeded, reduce fields, levels, sources or the time range. A bounded subset must not be interpreted as a complete result. Keep request_id with diagnostics. Use bounded retries with backoff; do not retry validation errors without changing the request.

Request errors use error.code and error.message. An HTTP 503 response for entirely unavailable sources may retain the regular results with issues; inspect error first, then per-query results.

Browser use and CORS

On the portal page fetch("/api/weather/query", …) uses the same origin. External applications should use the full portal URL; public read access supports CORS and OPTIONS preflight for POST application/json. Use credentials:"omit"; do not send portal cookies to an external site.

Cancel a request when the point changes
const controller = new AbortController();
const pending = fetch('/api/weather/query', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  credentials: 'omit',
  signal: controller.signal,
  body: JSON.stringify(request)
});
// When another point is selected:
controller.abort();

Canceling fetch prevents a stale result from being applied in the UI; it does not guarantee immediate server-side SQL cancellation. Interactive maps should keep current values, forecasts and features independent, and check the selected-point identity before displaying a result.

Aerogram through the common API

The Portal and SDK aerogram obtain forecast times through availability and the atmospheric column through samples. Both use POST /api/weather/query. SDK getSounding() converts the common fields/samples into the sounding-1 rendering model without calling a separate profile API.

Request example
{
  "schema_version": "weather-query-1",
  "point": {
    "lat": 55.41,
    "lon": 37.91
  },
  "queries": [
    {
      "id": "availability",
      "kind": "availability",
      "source": {
        "model": "noaa"
      }
    },
    {
      "id": "sounding",
      "kind": "samples",
      "source": {
        "model": "noaa"
      },
      "preset": "atmospheric_profile",
      "time": {
        "at": "2026-10-21T18:00:00Z",
        "selection": "nearest",
        "max_offset_seconds": 86400,
        "tie_break": "earlier"
      }
    }
  ]
}

preset:"atmospheric_profile" returns only the native parameters and levels required by the aerogram: TMP, DPT, RH, HGT, PRES, UGRD and VGRD. Read each field unit: values are not always normalized. Do not combine different valid_time samples; preserve gaps as null. HGT is not replaced with standard-atmosphere estimates; missing DPT may be derived from TMP/RH with an explicit quality flag.

The latest model run is selected by default. To select a specific run, pass run:{policy:"exact",at:"…Z"} to both queries. Availability does not establish coverage at the selected point. The old /api/grib/profile and /api/grib/profile-series endpoints are unchanged; other consumers may still use them.

Migration from existing weather APIs

Scenario mapping
Existing scenarioNew query
/api/weather/pointsamples; forecasts use a separate samples query with time.from/grid:native.
/api/weather/point-infoRasters use samples; features and observations use features.
/api/weather/point-contextOPMET/vectors use features; column uses diagnostics; extra uses samples fields:"all".
/api/weather/point-forecastdaily with preset:near_surface_weather; numerical-only requests use samples.
/api/weather/point-forecast/datesavailability

The listed legacy routes have been removed from the Portal and Hub and return 404. Martin Tiles no longer exposes /api/weather/point either. Use POST /api/weather/query; there are no redirects or network fallbacks to the retired APIs. Internal Hub consumers use the same query engine.

The URLs and contracts of /api/grib/profile, /api/grib/profile-series and /api/grib/getMeteoByTypeInPoint are unchanged. The Portal and SDK aerogram now use the common API as described above; other legacy consumers migrate separately.

Atmospheric profile inputs

For samples, preset: "atmospheric_profile" replaces fields/layers and selects TMP, DPT, RH, UGRD, VGRD and HGT at available pressure levels; PRES/HGT at the surface; TMP/DPT/RH at 2 m and UGRD/VGRD at 10 m. The response keeps the common fields/samples structure, native units, quality and missing-value markers. It does not calculate diagnostics or invent missing levels. Batch availability and samples in one request to obtain dates and the profile from one snapshot. The SDK uses this batch and does not issue a separate capabilities request before opening the aerogram.