# weatherapi HTTP API

This is the canonical public HTTP contract for `weatherapi`. The service is a
read-only API over the latest weather records available in the configured
weatherfeeder-populated Postgres database.

## Base URL

All paths are relative to the deployment root:

```text
http://localhost:8080
```

Use your deployment host in production.

## Authentication

`weatherapi` does not implement authentication or authorization. Put access
control in front of the service when a deployment requires it.

## Response Envelope

Successful JSON and XML responses use a top-level envelope:

```json
{
  "data": {}
}
```

When no latest/current resource exists, the request still succeeds and returns
`data: null`.

Text responses are rendered from endpoint-specific templates. When the payload
is missing, the templates return a short no-data message.

## Formats

Every implemented endpoint can produce:

| Format | Media type |
| --- | --- |
| `json` | `application/json` |
| `xml` | `application/xml` |
| `text` | `text/plain` |

Format selection is:

1. `format` query parameter;
2. `Accept` header;
3. configured `server.default_format`.

`format` values are case-insensitive. Unsupported formats return `406
Not Acceptable` with an error envelope.

## Shared Query Rules

Unknown query parameters are rejected with `400 Bad Request`.

| Parameter | Values | Default | Supported on |
| --- | --- | --- | --- |
| `format` | `json`, `xml`, `text` | configured default | all endpoints |
| `units` | `metric`, `us` | `metric` | all endpoints |
| `precision` | integer `0` through `2` | `0` | observations, current conditions, forecasts |
| `tz` or `TZ` | timezone selector | UTC/no conversion | forecasts, discussions, weather stories, outlooks |

`units`, `format`, and `precision` values are normalized case-insensitively
where applicable. `units=metric` returns metric field names; `units=us` returns
US-customary field names for unit-bearing payloads. Alerts, discussions, and
weather stories accept `units` but their current payload fields are not
materially changed by it. Outlooks also accept `units` without changing payload
values or field names.

`precision` controls numeric rounding. The default `0` rounds to whole numbers.
`precision` is rejected on alerts, discussions, weather stories, and outlooks.

Timezone selectors accepted by `tz` / `TZ`:

- IANA timezone names such as `America/Chicago`;
- common US abbreviations such as `CDT`, `CST`, `EDT`, `EST`, `MDT`, `MST`,
  `PDT`, and `PST`;
- UTC offsets in `+H`, `+HH`, `+HH:MM`, `-H`, `-HH`, or `-HH:MM` form, bounded
  to `-14:00` through `+14:00`;
- aliases `Chicago` and `Stl`, both mapped to `America/Chicago`.

If both `tz` and `TZ` are provided, their values must match
case-insensitively. Timezone conversion affects rendered timestamps and forecast
`/today` and `/tomorrow` day-slice filtering. Without a timezone parameter,
day-slice routes use UTC.

## Errors

API errors use a stable error envelope:

```json
{
  "error": {
    "code": "invalid_parameter",
    "message": "..."
  }
}
```

Implemented API error statuses:

| Status | Code | Cause |
| --- | --- | --- |
| `400 Bad Request` | `invalid_parameter` | unknown query parameter, invalid parameter value, invalid timezone, conflicting `tz` / `TZ`, or unsupported parameter on a route |
| `406 Not Acceptable` | `unsupported_format` | requested response format is not supported by the endpoint/renderers |

Unhandled service or database errors are returned as server errors by the
runtime.

## Behavior Not Implemented

`weatherapi` does not implement pagination, cache-control headers, rate-limit
headers, idempotency keys, retries, writes, or historical browsing outside the
implemented latest-resource and forecast day-slice routes.

## Endpoints

### Observations

```http
GET /observations
```

Returns the latest weather observation.

Query parameters: `format`, `units`, `precision`.

Metric `data` fields:

| Field | Type | Notes |
| --- | --- | --- |
| `stationId`, `stationName` | string | optional |
| `timestamp` | RFC3339 datetime | required when `data` is not null |
| `conditionCode` | integer | WMO weather code |
| `isDay` | boolean | optional |
| `textDescription` | string | optional |
| `temperatureC`, `dewpointC`, `apparentTemperatureC` | number | optional |
| `windDirectionDegrees`, `windSpeedKmh`, `windGustKmh` | number | optional |
| `barometricPressurePa`, `visibilityMeters` | number | optional |
| `relativeHumidityPercent` | number | optional |
| `presentWeather` | array | optional |

US mode replaces unit-bearing fields with `temperatureF`, `dewpointF`,
`apparentTemperatureF`, `windSpeedMph`, `windGustMph`,
`barometricPressureInHg`, and `visibilityMiles`. Direction and percentage
fields keep the same names.

Example:

```http
GET /observations?units=us&precision=1
```

### Current Conditions

```http
GET /conditions/current
```

Returns current conditions aggregated from recent `observations` rows. The
implemented observation window is 30 minutes.

Query parameters: `format`, `units`, `precision`.

Common `data` fields:

| Field | Type | Notes |
| --- | --- | --- |
| `conditionText` | string | optional text derived from WMO code and day/night flag |
| `isDay` | boolean | optional |
| `relativeHumidityPercent` | number | optional |
| `windDirectionDegrees` | number | optional |

Metric fields: `temperatureC`, `apparentTemperatureC`, `dewpointC`,
`windSpeedKmh`.

US fields: `temperatureF`, `apparentTemperatureF`, `dewpointF`,
`windSpeedMph`.

Example:

```http
GET /conditions/current?format=json&precision=0
```

### Active Alerts

```http
GET /alerts/active
```

Returns the latest active-alert snapshot.

Query parameters: `format`, `units`.

Run `data` fields:

| Field | Type | Notes |
| --- | --- | --- |
| `locationId`, `locationName` | string | optional |
| `asOf` | RFC3339 datetime | required when `data` is not null |
| `latitude`, `longitude` | number | optional |
| `alerts` | array | active alerts, possibly empty |

Alert fields include `id`, `event`, `headline`, `severity`, `urgency`,
`certainty`, `status`, `messageType`, `category`, `response`, `description`,
`instruction`, `sent`, `effective`, `onset`, `expires`, `areaDescription`,
`senderName`, and `references`. Most alert fields are optional except `id` when
an alert item is present.

Reference fields are `id`, `identifier`, `sender`, and `sent`.

Example:

```http
GET /alerts/active?format=text
```

### Convective Outlooks

```http
GET /outlooks/convective
GET /outlooks/convective/active
GET /outlooks/convective/location
```

Returns the latest SPC convective outlook run reconstructed from
weatherfeeder-owned Postgres tables.

Route behavior:

- `/outlooks/convective` returns the latest run with all stored outlook
  polygons unless user filters are supplied.
- `/outlooks/convective/active` adds an active-time filter using the server's
  current UTC time. Outlooks are active when `validFrom <= now < validTo`.
- `/outlooks/convective/location` adds the same active-time filter and
  `containsLocation=true`.

When no latest run exists, `data` is null. When a run exists but filters match
no outlooks, `data` remains an object and `outlooks` is an empty array.

Query parameters:

| Parameter | Supported on | Values |
| --- | --- | --- |
| `format`, `units`, `tz` / `TZ` | all outlook routes | shared rules above |
| `day` | all outlook routes | `1`, `2`, or `3` |
| `outlookType` | all outlook routes | `categorical`, `tornado`, `hail`, or `wind` |
| `containsLocation` | `/outlooks/convective`, `/outlooks/convective/active` | boolean |

`outlookType` values are normalized case-insensitively. `containsLocation` is
rejected on `/outlooks/convective/location` because that route always applies
`containsLocation=true`. `precision` and unknown parameters are rejected.

Run `data` fields:

| Field | Type | Notes |
| --- | --- | --- |
| `locationId`, `locationName` | string | optional |
| `latitude`, `longitude` | number | optional |
| `asOf` | RFC3339 datetime | required when `data` is not null |
| `issuedAt` | RFC3339 datetime | optional |
| `outlooks` | array | ordered outlook polygons, possibly empty |

Outlook fields:

| Field | Type | Notes |
| --- | --- | --- |
| `id`, `provider`, `product`, `outlookType`, `label` | string | required when an outlook is present |
| `day` | integer | SPC outlook day |
| `labelText`, `forecaster` | string | optional |
| `severityRank` | integer | optional |
| `validFrom`, `validTo`, `issuedAt`, `expiresAt` | RFC3339 datetime | required when an outlook is present |
| `sourceUrl`, `imageUrl` | string | optional |
| `containsLocation` | boolean | whether the outlook polygon contains the configured location |
| `geometry` | GeoJSON | stored outlook geometry |

GeoJSON coordinates use standard GeoJSON coordinate order: longitude, then
latitude. Timezone conversion applies to run `asOf`, run `issuedAt`, and each
outlook's `validFrom`, `validTo`, `issuedAt`, and `expiresAt`. Active filtering
compares instants and is not changed by the presentation timezone.

Examples:

```http
GET /outlooks/convective?day=1&outlookType=categorical
GET /outlooks/convective/location?format=text&tz=CDT
```

Example JSON response:

```json
{
  "data": {
    "locationId": "stl",
    "locationName": "St. Louis",
    "asOf": "2026-06-11T18:00:00Z",
    "issuedAt": "2026-06-11T17:00:00Z",
    "outlooks": [
      {
        "id": "spc-day1-cat-slight",
        "provider": "spc",
        "product": "convective",
        "day": 1,
        "outlookType": "categorical",
        "label": "SLGT",
        "labelText": "Slight Risk",
        "severityRank": 5,
        "validFrom": "2026-06-11T18:00:00Z",
        "validTo": "2026-06-12T12:00:00Z",
        "issuedAt": "2026-06-11T17:00:00Z",
        "expiresAt": "2026-06-12T12:00:00Z",
        "containsLocation": true,
        "geometry": {
          "type": "Polygon",
          "coordinates": [
            [
              [-91.0, 38.0],
              [-90.0, 38.0],
              [-90.0, 39.0],
              [-91.0, 38.0]
            ]
          ]
        }
      }
    ]
  }
}
```

Example location-filtered JSON response:

```json
{
  "data": {
    "locationId": "stl",
    "asOf": "2026-06-11T18:00:00Z",
    "outlooks": [
      {
        "id": "spc-day1-tor-2pct",
        "provider": "spc",
        "product": "convective",
        "day": 1,
        "outlookType": "tornado",
        "label": "2%",
        "validFrom": "2026-06-11T18:00:00Z",
        "validTo": "2026-06-12T12:00:00Z",
        "issuedAt": "2026-06-11T17:00:00Z",
        "expiresAt": "2026-06-12T12:00:00Z",
        "containsLocation": true,
        "geometry": {
          "type": "Point",
          "coordinates": [-90.2, 38.6]
        }
      }
    ]
  }
}
```

Text format uses the shared convective outlook template for all three outlook
routes and renders a no-data message when `data` is null.

### Forecasts

```http
GET /forecast/hourly
GET /forecast/hourly/today
GET /forecast/hourly/tomorrow
GET /forecast/narrative
GET /forecast/narrative/today
GET /forecast/narrative/tomorrow
```

Returns the latest hourly or narrative forecast run. `/today` and `/tomorrow`
return a copy of the latest run with `periods` filtered by each period's
`startTime` in the resolved timezone. If no periods match, `data` remains an
object and `periods` is an empty array.

Query parameters: `format`, `units`, `precision`, `tz` / `TZ`.

Run `data` fields:

| Field | Type | Notes |
| --- | --- | --- |
| `locationId`, `locationName` | string | optional |
| `issuedAt` | RFC3339 datetime | required when `data` is not null |
| `updatedAt` | RFC3339 datetime | optional |
| `product` | string | `hourly` or `narrative` for implemented routes |
| `latitude`, `longitude` | number | optional |
| `elevationMeters` or `elevationFeet` | number | optional, depends on `units` |
| `periods` | array | ordered forecast periods |

Metric period fields:

`startTime`, `endTime`, `name`, `isDay`, `conditionCode`,
`textDescription`, `temperatureC`, `temperatureCMin`, `temperatureCMax`,
`dewpointC`, `relativeHumidityPercent`, `windDirectionDegrees`,
`windSpeedKmh`, `windGustKmh`, `barometricPressurePa`, `visibilityMeters`,
`apparentTemperatureC`, `cloudCoverPercent`,
`probabilityOfPrecipitationPercent`, `precipitationAmountMm`,
`snowfallDepthMm`, and `uvIndex`.

US mode uses the same non-unit fields and replaces unit-bearing fields with
`temperatureF`, `temperatureFMin`, `temperatureFMax`, `dewpointF`,
`windSpeedMph`, `windGustMph`, `barometricPressureInHg`, `visibilityMiles`,
`apparentTemperatureF`, `precipitationAmountIn`, `snowfallDepthIn`, and
`elevationFeet` at run level.

`startTime` and `endTime` are required for each period. Other period fields are
optional, including `conditionCode`; narrative forecasts may omit it.

Examples:

```http
GET /forecast/hourly?units=us&precision=1&TZ=-5
GET /forecast/narrative/tomorrow?format=text&tz=Chicago
```

### Forecast Discussions

```http
GET /discussion
GET /discussion/key-messages
GET /discussion/short-term
GET /discussion/long-term
```

Returns the latest forecast discussion or a focused subresource.

Query parameters: `format`, `units`, `tz` / `TZ`.

Full discussion `data` fields:

| Field | Type | Notes |
| --- | --- | --- |
| `officeId`, `officeName` | string | optional |
| `product` | string | currently `afd` from weatherfeeder data |
| `issuedAt` | RFC3339 datetime | required when `data` is not null |
| `updatedAt` | RFC3339 datetime | optional |
| `keyMessages` | array of strings | optional |
| `shortTerm`, `longTerm` | object | optional section objects |

Discussion section fields are `qualifier`, `issuedAt`, and `text`, all
optional.

Subresources return the same metadata plus only their focused field:
`keyMessages`, `shortTerm`, or `longTerm`.

Example:

```http
GET /discussion/short-term?format=text&tz=CDT
```

### Weather Stories

```http
GET /weatherstories
GET /weatherstories/latest
```

`/weatherstories` returns the latest weather-story run and its ordered stories.
`/weatherstories/latest` returns the latest individual story.

Query parameters: `format`, `units`, `tz` / `TZ`.

Run `data` fields:

| Field | Type | Notes |
| --- | --- | --- |
| `officeId` | string | optional |
| `asOf` | RFC3339 datetime | required when `data` is not null |
| `stories` | array | ordered story objects |

Story fields:

| Field | Type | Notes |
| --- | --- | --- |
| `officeId` | string | optional |
| `startTime`, `endTime`, `updatedAt` | RFC3339 datetime | required when a story is present |
| `title`, `description`, `altText`, `downloadUrl` | string | optional |
| `priority` | boolean | required when a story is present |
| `order` | integer | required when a story is present |

Examples:

```http
GET /weatherstories?tz=America/Chicago
GET /weatherstories/latest?format=xml
```

## Copyable Requests

See [`examples/requests.http`](../examples/requests.http) for a compact set of
requests covering the implemented endpoint families.
