# NWS Climate Data API - Daily Temperature by Weather Station (`neverempty/us-climate-reports`) Actor

Official NOAA / National Weather Service daily climate report (CLI) for 629 US weather stations: the certified daily high and low temperature with the time each occurred, records, normals, departure, precipitation and snowfall. Missing (MM) and trace (T) are never turned into zero.

- **URL**: https://apify.com/neverempty/us-climate-reports.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** News, Developer tools, MCP servers
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$10.00 / 1,000 climate day returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## NWS Climate Data API - Daily Temperature by Weather Station

**The certified daily climate report for 629 US stations, parsed into clean rows.**

Every morning the US National Weather Service publishes a **Climatological Report (CLI)** for each of its climate sites: the official daily maximum and minimum temperature, the exact time each occurred, the record for that date, the 30-year normal, the departure from normal, precipitation, snowfall, degree days, wind and humidity.

It is published as a **fixed-width plain-text bulletin**. This Actor turns it into rows you can actually use.

This reads `api.weather.gov`, the official public API of the **US National Weather Service (NOAA)**. It is a work of the US government: public domain, no API key, no rate-limit contract to sign, no terms that forbid redistribution. Nothing here parses a website, so nothing here breaks when a website is redesigned. No proxies required.

### Why not just parse it yourself

Because the bulletin has four traps, and getting any of them wrong gives you a number that looks fine and is wrong.

| Trap | What a naive parser does | What this Actor does |
|---|---|---|
| **`MM` means the instrument reported nothing** | Writes `0` | `null`, plus `maxTemperatureIsMissing: true` |
| **`T` means trace** - it did rain, just less than 0.005 in | Writes `0` ("it did not rain") | `null`, plus `precipitationIsTrace: true` |
| **A report headed `TODAY` is preliminary** - the day is not over, the high can still rise | Treats it as the settled daily high | `reportBasis: "PRELIMINARY"` and the time it was valid as of |
| **`R` marks a record set or tied** - and it shares the cell with the value, as in `T   R` | Drops it, or drops the whole row | `precipitationIsTrace: true` **and** `precipitationIsRecord: true` |
| **Different weather offices publish different columns** | Assumes one fixed layout, then silently drops or misreads rows | Reads each bulletin's own column header |

Every row also carries `coreFieldsObserved` / `coreFieldsTotal`, so you can tell at a glance whether a station actually reported or quietly went dark.

#### The layout trap, concretely

The bulletin is not one national fixed-width format. Three real examples from the same morning:

```
NYC   WEATHER ITEM   OBSERVED TIME   RECORD YEAR NORMAL DEPARTURE LAST
HKS   WEATHER ITEM   OBSERVED TIME               NORMAL DEPARTURE LAST
LWC   WEATHER ITEM   OBSERVED                    NORMAL DEPARTURE LAST
```

On top of that, three-digit temperatures push the columns left - Phoenix at 113 F and Dallas at 103 F do not line up with New York at 76 F. A parser with hard-coded column positions loses exactly the days that matter most.

This Actor reads the column header of each bulletin and assigns values from that. **13 different header layouts are in live use.** Verified against all **629 stations and 10,376 table rows**, cross-checked against an independently written oracle: zero values taken from the wrong column, zero rows dropped, zero unexplained blanks.

### What you get

One row per station, per climate day:

| Field | Example | Notes |
|---|---|---|
| `locationCode` / `stationName` | `NYC` / `CENTRAL PARK NY` | The station as the NWS names it |
| `climateDate` | `2026-08-29` | The climate day the report covers, **not** the day it was issued |
| **`reportBasis`** | **`FINAL`** | `FINAL` = the completed previous day. `PRELIMINARY` = still in progress |
| `validAsOfLocal` | `0425 PM LOCAL TIME` | Only on preliminary reports |
| **`maxTemperatureF`** | **`76`** | The official daily high |
| **`maxTemperatureTimeLst`** | **`355 PM`** | When it occurred, local standard time |
| `maxTemperatureRecordF` / `maxTemperatureRecordYears` | `99` / `[1953]` | Record for this date, and every year it was set |
| `maxTemperatureNormalF` / `maxTemperatureDepartureF` | `82` / `-6` | 1991-2020 normal and departure |
| `maxTemperatureLastYearF` | `78` | Same date last year |
| `maxTemperatureIsRecord` | `false` | `true` when the NWS flags a record set or tied |
| `minTemperature*` | | Same nine fields for the daily low |
| `averageTemperatureF` + normal + departure | `70` / `74` / `-4` | |
| `precipitationInches` (+ `IsTrace`, `IsMissing`) | `0` | Plus normal, month-to-date, since Jan 1 |
| `snowfallInches` (+ `IsTrace`), `snowDepthInches` | `0` | Plus month-to-date |
| `heatingDegreeDays` / `coolingDegreeDays` | `0` / `5` | |
| `highestWindSpeedMph`, `highestGustSpeedMph`, `averageWindSpeedMph`, `highestWindDirection` | `13`, `19`, `3.6`, `NE (40)` | |
| `highestRelativeHumidityPercent` / `lowestRelativeHumidityPercent` | `73` / `38` | |
| `averageSkyCover` | `0` | 0 to 1 |
| `coreFieldsObserved` / `coreFieldsTotal` | `9` / `9` | How much of the report was actually observed |
| `issuanceTime`, `productId`, `issuingOffice` | `2026-08-30T06:42:00+00:00`, `KOKX` | So you can cite the exact bulletin |
| `source` | `NWS api.weather.gov CLI product (official, public domain)` | |

#### One real row, exactly as the Actor writes it

```json
{
  "locationCode": "NYC",
  "stationName": "CENTRAL PARK NY",
  "climateDate": "2026-08-29",
  "reportBasis": "FINAL",
  "reportBasisNote": "Issued for the completed previous climate day.",
  "validAsOfLocal": null,
  "issuanceTime": "2026-08-30T06:42:00+00:00",
  "issuingOffice": "KOKX",
  "maxTemperatureF": 76,
  "maxTemperatureTimeLst": "355 PM",
  "maxTemperatureIsMissing": false,
  "maxTemperatureIsRecord": false,
  "maxTemperatureRecordF": 99,
  "maxTemperatureRecordYears": [1953],
  "maxTemperatureNormalF": 82,
  "maxTemperatureDepartureF": -6,
  "maxTemperatureLastYearF": 78,
  "minTemperatureF": 64,
  "minTemperatureTimeLst": "719 AM",
  "minTemperatureRecordF": 50,
  "minTemperatureRecordYears": [1965, 1982, 1986],
  "minTemperatureNormalF": 67,
  "minTemperatureDepartureF": -3,
  "averageTemperatureF": 70,
  "averageTemperatureDepartureF": -4,
  "precipitationInches": 0,
  "precipitationIsTrace": false,
  "precipitationMonthToDateInches": 8.49,
  "precipitationSinceJan1Inches": 31.2,
  "snowfallInches": 0,
  "snowDepthInches": 0,
  "heatingDegreeDays": 0,
  "coolingDegreeDays": 5,
  "highestWindSpeedMph": 13,
  "highestGustSpeedMph": 19,
  "averageWindSpeedMph": 3.6,
  "highestRelativeHumidityPercent": 73,
  "coreFieldsObserved": 9,
  "coreFieldsTotal": 9,
  "source": "NWS api.weather.gov CLI product (official, public domain)"
}
```

And a preliminary one, from a station that was not reporting - note that nothing is faked as zero:

```json
{
  "locationCode": "ANC",
  "stationName": "ANCHORAGE AK",
  "climateDate": "2026-08-29",
  "reportBasis": "PRELIMINARY",
  "validAsOfLocal": "0400 PM LOCAL TIME",
  "maxTemperatureF": null,
  "maxTemperatureTimeLst": null,
  "maxTemperatureIsMissing": true,
  "maxTemperatureRecordF": 72,
  "maxTemperatureNormalF": 61,
  "coreFieldsObserved": 3,
  "coreFieldsTotal": 9
}
```

And a station whose weather office publishes no record or time columns at all - the fields that do not exist come back as `null`, not as zero:

```json
{
  "locationCode": "LWC",
  "stationName": "LAWRENCE KS",
  "climateDate": "2026-08-29",
  "reportBasis": "FINAL",
  "maxTemperatureF": 98,
  "maxTemperatureTimeLst": null,
  "maxTemperatureRecordF": null,
  "maxTemperatureNormalF": 88,
  "coreFieldsObserved": 8,
  "coreFieldsTotal": 9
}
```

### Input

```json
{
  "locations": ["NYC", "LAX", "ORD", "MIA", "DFW", "DEN", "PHL", "ATL", "SEA", "BOS", "IAH", "PHX"],
  "reportsPerLocation": 1,
  "maxRetries": 3
}
```

- **`locations`** - three-letter NWS climate site codes. 629 sites publish a daily report; the full list is at `https://api.weather.gov/products/types/CLI/locations`. Defaults to twelve major US cities.
- **`reportsPerLocation`** - how many recent reports per station. `1` is the latest. Raise it to backfill; the NWS keeps roughly the last two weeks per site.
- **`maxRetries`** - retries per station when the NWS returns an error or something that is not JSON.

### Typical uses

- **Settling and grading daily temperature contracts** against the official certified high or low, with the time it occurred.
- **Energy and utility load models** - heating and cooling degree days per station per day, straight from the source.
- **Insurance and agriculture** - precipitation, snowfall and snow depth with trace distinguished from zero.
- **Climate and records reporting** - departure from normal, the record for the date, and every year the record was set.

### What it does when something is wrong

It tells you, instead of returning a clean-looking empty result.

- A station code that is not three or four characters is **rejected before any request is made**.
- A station with no reports **throws with the reason**, and the run continues with the other stations.
- A bulletin that is empty, or in one of the non-standard Pacific-office formats, is **never delivered as a row of nulls**. It is reported as unreadable and not charged - a row that says `FINAL` with every field blank is worse than no row.
- If the NWS returns HTML or an error status, that is treated as **blocked** and retried, then recorded in the `RUN_ISSUES` key-value record.
- If a bulletin's columns do not line up with its own header, that row is **skipped rather than guessed at** - values are never read out of the neighbouring column, and a value that lands outside every declared column is never quietly discarded.
- If you set a maximum total charge and the run hits it, the run **stops and says the result is incomplete** rather than quietly delivering a short dataset.

### Pricing

Pay per result: one charge per climate-day row delivered. Failed stations are not charged.

### Source and licence

All data comes from the **US National Weather Service** (`api.weather.gov`), a work of the US federal government and therefore in the public domain. This Actor adds parsing, validation and the missing/trace/preliminary handling described above.

# Actor input Schema

## `locations` (type: `array`):

Three-letter National Weather Service climate site codes, for example "NYC" (Central Park), "LAX", "ORD". 629 sites publish a daily climate report. The full list is at https://api.weather.gov/products/types/CLI/locations . Leave the default to get twelve major US cities.

## `reportsPerLocation` (type: `integer`):

How many of the most recent climate reports to return per station. 1 gives you the latest one. Raise it to backfill previous days - the National Weather Service keeps roughly the last two weeks per site.

## `maxRetries` (type: `integer`):

How many times to retry a station if the National Weather Service returns an error or something that is not JSON.

## Actor input object example

```json
{
  "locations": [
    "NYC",
    "LAX",
    "ORD",
    "MIA",
    "DFW",
    "DEN",
    "PHL",
    "ATL",
    "SEA",
    "BOS",
    "IAH",
    "PHX"
  ],
  "reportsPerLocation": 1,
  "maxRetries": 3
}
```

# Actor output Schema

## `results` (type: `string`):

All daily climate report rows produced by this run.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/us-climate-reports").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("neverempty/us-climate-reports").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call neverempty/us-climate-reports --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/us-climate-reports"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/1PYAmQC0pLcao43Rp/builds/nssu3SPncsMplEyoE/openapi.json
