# NOAA METAR Weather Station Data Extractor (`zinin/noaa-metar-weather-extractor`) Actor

Extract NOAA METAR observations for selected weather stations, preserving source reports, measurement units, age checks and clear missing or partial results.

- **URL**: https://apify.com/zinin/noaa-metar-weather-extractor.md
- **Developed by:** [Tim Zinin](https://apify.com/zinin) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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

## NOAA METAR Weather Station Data Extractor

Export recent METAR observations for selected stations, with source reports, explicit units and age checks for weather tables and operational analytics.

**Evidence status: private build accepted on 2026-09-05.** Every input/output example below is an exact dataset row exported from the accepted private acceptance runs on Apify's network, and the tariff in R4 was read back from the listing's pay-per-event configuration after those runs. This page still makes no claim that a scheduled task or a public review history already exists.

### R1. What you get

This Actor retrieves METAR observations from the official Aviation Weather Center Data API for a bounded list of station identifiers. Each complete fresh observation becomes one dataset row with the station, observation time, original report and selected decoded measurements. The unit of content is an **observation**, identified by station and source observation time. It is not a forecast, a weather alert, a flight recommendation or a settlement calculation.

The output keeps temperature and dewpoint in Celsius, wind speed in knots, visibility in statute miles and altimeter setting in hectopascals. It preserves source qualifiers such as "10+" instead of pretending they are exact measured values. Missing measurements remain null. When the source reports variable wind as VRB, windDirectionRaw keeps that value and windDirectionDeg is null. No arbitrary numeric bearing is substituted.

Freshness is part of the result contract. The Actor calculates age from the observation timestamp and compares it with maxAgeMinutes. A report beyond your selected age threshold is emitted as a free stale row with its evidence retained, rather than being billed as a current observation. The check does not guarantee instantaneous source publication or the absence of later corrections; it makes the age at this processing step explicit.

A source request that returns no data is different from a failed request. HTTP 204 produces a clean not\_found row for the requested station. A network problem, unexpected JSON shape or inconsistent record produces source\_error with a nonempty error. If one station has a usable observation and another part of the batch fails, the complete fresh observation can still be delivered, accompanied by status rows and a partial indication for the batch.

The default input asks for KJFK and EGLL, the latest report at each station, a maximum of 20 fresh observations and an age threshold of 180 minutes. These two station identifiers make a small real source check possible without a buyer API key. They are examples of requested stations, not a promise that every station in the source will report at every moment. Availability and reporting intervals vary.

### R2. Who uses it

Operations analysts can place current station observations beside airport-related logistics data in a dashboard. The source report and time remain available for a human to inspect when a measured value changes. The Actor helps build an evidence table; it does not decide whether a shipment, vehicle, flight or person should proceed. Those decisions need the organization's own rules and appropriate operational information.

Data teams can populate a time-series table for a small set of facilities. A stable station-time identity supports deduplication, while observedAt and checkedAt separate the measurement time from the retrieval time. A team can retain rawMetar for later interpretation and keep decoded columns for filters and charts. Repeated observations across runs are normal when the source has not issued a newer report.

Researchers can collect bounded recent history for a specific comparison, such as temperature and wind conditions around a known operational event. hours selects a recent source window, and maxAgeMinutes determines which observations are eligible as fresh paid content. These controls work together: choosing a 24-hour source window while keeping a three-hour age threshold deliberately excludes older reports from paid results.

Dashboard developers can use explicit statuses to display missing, stale and failed data differently. A gray “no report available” cell is not the same as a red “source request failed” cell, and an older value should retain its timestamp if shown at all. The Actor supplies the distinction; your user interface chooses the presentation. Do not hide an old observation's age simply because its numeric temperature looks plausible.

This product is not intended for global METAR mirroring, station discovery, airport-name geocoding, TAF forecasting or official aviation briefing. It accepts known station identifiers and makes a limited batch request. There is no route to arbitrary weather URLs and no background download of the worldwide cache. If your task requires those other products, its source and output contract must be specified separately.

### R3. How to run

Start with the prefilled stationIds array \["KJFK","EGLL"], hours 0, maxObservations 20 and maxAgeMinutes 180. Run the Actor, wait for a terminal status, then open the dataset and the OUTPUT summary. With available fresh reports, the dataset contains found=true observations for the selected stations. Check observedAt and ageMinutes before treating them as recent, and retain rawMetar if you need to inspect the original report.

Use four-character ICAO-style station identifiers, not airport display names or three-character IATA codes. For example, KJFK is accepted while JFK is rejected by the input shape. The Actor does not infer whether a four-character identifier corresponds to a currently reporting station. A syntactically valid identifier can still receive no observation from the source. That outcome is a source check, not automatic input validation failure.

Lowercase station IDs are normalized to uppercase, and duplicate normalized IDs are removed within the run. Leading or trailing spaces are not silently trimmed: correct them in the input. Exact normalization helps prevent visually similar values from creating different request shapes. The array can contain between one and twenty identifiers, and the runtime enforces this ceiling even for direct API callers.

For recent history, set hours to an integer from 1 through 24. Set maxAgeMinutes consistently with the history you actually want to retain as paid observations. To accept reports across a full 24-hour window, use 1440 minutes. A narrower threshold intentionally makes older entries stale. maxObservations applies across all requested stations, not separately to each station, so a history query can reach the shared result limit quickly.

The dataset view exposes identity, measurements, freshness, status and source provenance. Use the JSON export when working with clouds or a visibility qualifier. A CSV export can be useful for a spreadsheet, but text such as "10+" must stay text and nested cloud layers need an explicit representation. A consumer that converts every cell to a number can destroy the very qualifiers this Actor preserves.

### R4. Pricing

**Pricing: $0.003 per start plus $0.0005 per complete fresh observation.** This is the listing's configured pay-per-event tariff, read back after the accepted private runs on 2026-09-05: the start event is apify-actor-start and the result event is result-found; no other event is priced. One observation is a unique station-plus-observation-time record delivered with found=true. The result event is result-found. The price is not multiplied by the number of decoded measurement columns or cloud layers in that row.

At this rate, a run with one fresh observation is $0.0035; two observations are $0.004; twenty are $0.013; and four hundred are $0.203. These arithmetic examples assume the configured 256 MB run with the platform's minimum start-event count of one. The start event is charged once per gigabyte of run memory: the default 256 MB run counts as one start unit ($0.003), while a run launched with 4 GB counts as four ($0.012). This was observed in the acceptance runs, which were deliberately launched at 4 GB and recorded four start events each.

Stale observations, HTTP 204 absence rows, source errors, invalid input explanations and partial notices do not carry the result-found event. A repeated station-time identity within the same run is deduplicated. Missing temperature, wind or cloud values do not alone make an otherwise complete source observation free: optional measurements can legitimately be absent. Completeness requires valid identity, time and raw report, not a fabricated value for every column.

A run can incur its automatic start charge even when every station returns no data or every observation is stale. That distinction matters for polling costs. A schedule that repeatedly requests the same small station set before the source updates can pay for repeated observations or empty starts. Deduplication within one run does not provide a free cross-run cache. Decide your cadence from the useful observation frequency and your own reporting requirements.

The Actor checks remaining money before each paid row and serializes that check with the linked dataset-and-charge operation. If a row does not fit, it is withheld and paid work stops. A row that fits exactly remains delivered and paid even if the SDK announces that the event limit has now been reached. The output records budget exhaustion separately from source failure; an exhausted cap does not prove the source lacked further observations.

Pricing and budget failures use a fail-closed path. If the platform pricing is unavailable, result-found is unpriced or automatic dataset writes themselves cost money, the Actor fails without risking a charged explanation row. If a later budget read fails, it also fails. A thrown linked write/charge can leave delivery uncertain, so the Actor stops and asks for reconciliation through its status and summary rather than retrying a possibly delivered observation.

### R5. Input contract

The input supports four fields. It must be a JSON object; unknown fields and incorrect types are rejected. The schema's bounds are repeated at runtime. Numbers must be JSON integers, not strings that happen to look numeric. There is no maxConcurrency input, arbitrary URL input, station-name search or setting that disables source validation.

| Field | Prefill/default | Bounds | Meaning |
|---|---|---|---|
| stationIds | \["KJFK","EGLL"] | 1–20 four-character alphanumeric strings | Selected stations, uppercased and deduplicated |
| hours | 0 | Integer 0–24 | 0 means latest; positive values request a recent history window |
| maxObservations | 20 | Integer 1–400 | Total fresh complete observations across the entire batch |
| maxAgeMinutes | 180 | Integer 15–1440 | Maximum age for a result to be fresh and billable |

hours=0 omits the upstream hours parameter. The API's latest-report response is reduced to the newest valid observation per station if multiple entries are present. For positive hours, distinct station-time observations can all be considered, subject to the common age and result limits. The Actor does not issue separate requests for every hour or create a station-by-hour Cartesian workload.

The source request batches all selected IDs. The response is capped at two million decoded bytes, and at most four hundred source records are considered. If the response reaches four hundred entries, the Actor conservatively flags possible upstream truncation. It does not know the exact number of records the source would have returned without its own cap. A missing station in that situation cannot be confidently described as having no available report.

The newest observation is considered first across the batch. If two reports have the same station and observation time, a later reportTime is preferred as the correction candidate, and only one row for that identity is retained. An equal-time tie does not create two paid observations. The Actor does not claim to preserve every correction edition; a historical correction archive would require a different identity and retention contract.

maxAgeMinutes is evaluated against obsTime, which becomes observedAt. It is not evaluated against the nominal reportTime or receiptTime. A report can have a nominal report time later than its actual observation time. That is not a reason to reset its age. Source observation times more than five minutes in the future are rejected as inconsistent rather than treated as indefinitely fresh; small clock differences remain visible through ageMinutes.

The freshness comparison uses the unrounded age internally. The exported ageMinutes is rounded to four decimal places for readability. A report exactly at the threshold is eligible; one older than the threshold is stale. In rare near-boundary cases, a displayed rounded age can look equal to the threshold while stale reflects the more precise comparison. Use stale as the Actor's outcome and observedAt for your own exact recalculation.

Requests are sequential and limited to at most one per second. Transient failures can receive two retries, with bounded pauses and Retry-After respected inside the remaining 240-second work deadline. Each request/DNS phase is bounded, and every redirect repeats the allowed-host and verified-DNS checks. The run timeout is 300 seconds. These are hard operating limits, so increasing maxObservations does not grant unlimited waiting or pagination.

### R6. Happy, partial and failure output

The examples in this section are exact dataset rows exported from the accepted private acceptance runs on 2026-09-05, executed on Apify's network with the pay-per-event tariff active. Your own run will have its own checkedAt, ageMinutes and weather values; the contract fields are what to compare.

**Example A — latest observation for one station (accepted run: one result-found event charged).**

```json
{"stationIds":["KJFK"],"hours":0,"maxObservations":20,"maxAgeMinutes":180}
```

```json
{"input":"KJFK","sourceUrl":"https://aviationweather.gov/api/data/metar?ids=KJFK&format=json","found":true,"status":"ok","resultCount":1,"partial":false,"error":"","warnings":[],"checkedAt":"2026-09-05T11:32:16.458Z","schemaVersion":"1.0","stationId":"KJFK","observationId":"KJFK:1788605460","observedAt":"2026-09-05T10:51:00.000Z","reportTime":"2026-09-05T11:00:00.000Z","rawMetar":"METAR KJFK 051051Z 35007KT 10SM FEW095 SCT140 22/14 A2983 RMK AO2 SLP100 T02170144 $","temperatureC":21.7,"dewpointC":14.4,"windDirectionDeg":350,"windDirectionRaw":350,"windSpeedKt":7,"visibilitySm":"10+","altimeterHpa":1010.2,"clouds":[{"cover":"FEW","base":9500},{"cover":"SCT","base":14000}],"ageMinutes":41.2743,"stale":false}
```

The observation was 41 minutes old at processing time and therefore fresh under the 180-minute threshold. A later run will have its own timestamps and can have different weather. The important contract is the station identity, source report, explicit units, qualifier preservation (note "10+") and freshness outcome, not an expectation that KJFK always has these values.

**Example B — clean no-data response (accepted run: zero result-found events charged).**

```json
{"stationIds":["ZZZZ"],"hours":0,"maxObservations":20,"maxAgeMinutes":180}
```

```json
{"input":"ZZZZ","sourceUrl":"https://aviationweather.gov/api/data/metar?ids=ZZZZ&format=json","found":false,"status":"not_found","resultCount":0,"partial":false,"error":"","warnings":[],"checkedAt":"2026-09-05T11:32:41.210Z","schemaVersion":"1.0","stationId":"ZZZZ","httpStatus":204}
```

The observed 204 confirms no data for that particular source request, not a universal claim about station validity. A four-character identifier can be syntactically acceptable while having no current report. If the source later changes its handling, the negative control must be rechecked. HTTP 400, 403 or a timeout must never be relabelled as this clean absence row.

**Example C — shared limit across two stations (accepted run: one result-found event charged, two dataset rows).**

```json
{"stationIds":["KJFK","EGLL"],"hours":0,"maxObservations":1,"maxAgeMinutes":180}
```

```jsonl
{"input":"EGLL","sourceUrl":"https://aviationweather.gov/api/data/metar?ids=KJFK%2CEGLL&format=json","found":true,"status":"ok","resultCount":1,"partial":false,"error":"","warnings":[],"checkedAt":"2026-09-05T11:32:48.722Z","schemaVersion":"1.0","stationId":"EGLL","observationId":"EGLL:1788607200","observedAt":"2026-09-05T11:20:00.000Z","reportTime":"2026-09-05T11:20:00.000Z","rawMetar":"METAR EGLL 051120Z AUTO 29005KT 210V350 9999 SCT043 21/08 Q1024","temperatureC":21,"dewpointC":8,"windDirectionDeg":290,"windDirectionRaw":290,"windSpeedKt":5,"visibilitySm":"6+","altimeterHpa":1024,"clouds":[{"cover":"SCT","base":4300}],"ageMinutes":12.812,"stale":false}
{"input":null,"sourceUrl":"https://aviationweather.gov/api/data/metar?ids=KJFK%2CEGLL&format=json","found":false,"status":"partial","resultCount":0,"partial":true,"error":"","warnings":["Complete observations withheld: 1; malformed observations: 0; upstream cap possibly reached: false."],"checkedAt":"2026-09-05T11:32:48.824Z","schemaVersion":"1.0","observationsWithheld":1}
```

Both stations had fresh complete observations; the newer one (EGLL, observed at 11:20 UTC) was delivered and the free partial notice reports the one withheld KJFK observation. Which station is first depends on observedAt, not the input array position: KJFK came first in the input and still lost to the newer EGLL report. The actual source can change between runs, so acceptance should assert one complete observation and the separate partial contract, without freezing a temperature or assuming KJFK must always win the newest-first ordering.

**Stale and error shapes (contract; not triggered in the accepted runs because every report was fresh and the source answered).** A stale row retains stationId, observedAt, rawMetar and available measurements, but has found=false, status=stale, resultCount=0, stale=true and error="". A source\_error row has found=false and resultCount=0 with a nonempty error. A valid but empty response is never inferred from a malformed JSON object. The difference is essential for dashboards that choose whether to display an older observation or an explicit missing state.

### R7. Field dictionary

All rows share an envelope for identity, provenance and outcome. Measurement columns belong to observation rows, including free stale observations where the source evidence is retained. An absence or transport-error row does not invent measurement fields. Branch on found/status before deciding whether a row belongs in a current-weather table.

| Field | Type | Meaning and handling |
|---|---|---|
| input | String or null | Requested station, or null for a batch-level notice |
| sourceUrl | String | Exact batched API request used for provenance |
| found | Boolean | True only for complete fresh observations delivered as results |
| status | String | ok, stale, not\_found, source\_error, incomplete\_batch, input\_error or partial |
| resultCount | Integer | One for fresh paid content, zero for free status/stale rows |
| partial | Boolean | Incomplete coverage indicator on a notice or incomplete station row |
| error | String | Empty for clean absence/stale/limits; nonempty for an actual error |
| warnings | Array | Scope, stale or partial explanations |
| checkedAt | ISO UTC string | Time the Actor prepared the row |
| schemaVersion | String | Row contract version, currently 1.0 |
| stationId | String | Source ICAO station identity, normalized request must match |
| observationId | String | stationId plus Unix observation time, separated by a colon |
| observedAt | ISO UTC string | Actual source observation time |
| reportTime | Source date-time string | Nominal report time, preserved separately from observation time |
| rawMetar | String | Original METAR/SPECI report text returned by the source |
| temperatureC | Number or null | Decoded air temperature in Celsius |
| dewpointC | Number or null | Decoded dewpoint in Celsius |
| windDirectionDeg | Number or null | Numeric source bearing; null for VRB or missing |
| windDirectionRaw | Number, string or null | Original direction value, including VRB |
| windSpeedKt | Number or null | Wind speed in knots |
| visibilitySm | Number, string or null | Statute miles, retaining qualifiers such as 10+ |
| altimeterHpa | Number or null | Altimeter setting in hectopascals |
| clouds | Array or null | Source cloud layers; each cover is a code and base is in feet when supplied |
| ageMinutes | Number | Minutes from observation time to processing, rounded for display |
| stale | Boolean | True when age exceeds maxAgeMinutes |
| httpStatus | Integer on clean absence rows | 204 or the source success status used for no-data handling |
| observationsWithheld | Integer on partial notice | Complete candidates excluded by result/budget delivery limits |

visibilitySm is intentionally not a number-only column. The source can state a lower-bound qualifier, and "10+" should not become exactly 10 in a statistical calculation. If your chart needs a numeric proxy, create a separate derived value with a qualifier flag and describe the method. Keep the original visibilitySm in the archive so later reviewers can recover what the source actually said.

altimeterHpa is mapped from the documented JSON altim value. The raw report may contain an A-group whose textual notation uses a different convention, but the Actor does not parse that text to overwrite the decoded hPa field. A consumer wanting inches of mercury should perform an explicit documented conversion in a separate column. Do not infer the unit from the size of a number.

clouds preserves the source layer array instead of flattening the first layer into a universal ceiling claim. A cloud-base value is in feet when present. Missing base remains missing/null in the source structure, and a cover code such as FEW or SCT is not itself a numeric altitude. This Actor does not calculate operational ceilings or flight categories from those layers. Keep any downstream classification separate from source measurements.

The OUTPUT record separates station coverage from observation counts. requested and processed refer to station identifiers in the batch; unprocessed refers to station work not attempted. observationsReceived counts the returned source array, observationsConsidered counts deduplicated candidates examined for delivery, and observationsWithheld tracks candidates not delivered because of caps or a stopped delivery channel. A history query can have many observations for one requested station.

delivered and paid count confirmed complete fresh result rows and their linked events. free counts delivered status or stale rows. failed counts free error rows, while partial and fatal describe the run as a whole. deliveryUncertain marks a write/charge outcome that cannot be safely called delivered-and-paid. replaySafe is false because a new run can deliver and bill the same station-time identity again.

### R8. Evidence and boundaries

The source is the [Aviation Weather Center Data API](https://aviationweather.gov/data/api/), specifically its METAR JSON endpoint. The build checked the current documentation and OpenAPI definitions for observation time, wind, visibility, pressure and clouds. The service documents bounded machine-to-machine access. This Actor stays on one source route and does not combine another weather API behind the same provenance field.

Evidence comes from three layers, and none substitutes for another. A raw API GET showed that reports were available. The exported processInput probe showed that the Actor's actual mapping and age logic handled the live response. The accepted private runs of 2026-09-05 on Apify's network then confirmed platform access to the source, the rows shown in R6, the pay-per-event charge counts (one result-found event per fresh observation, none for the 204 absence row and the partial notice) and the default 256 MB memory setting.

METAR reports are observations associated with stations and times. They do not describe every location between stations, forecast the next hour or certify current conditions at an arbitrary facility. The Actor does not interpolate, geocode an address or assess whether a station represents your operational site. Choose stations with that limitation in mind and keep the station identifier visible in any derived dashboard.

Freshness is measured at processing time. If a report is fresh when the run completes but your integration imports it much later, its usefulness can change. Recalculate age from observedAt when displaying a current-status view, and distinguish that display-time age from the stored ageMinutes. A successful historical run is not a continuously refreshed weather feed unless your own workflow schedules and validates new runs.

Latest mode returns the newest valid source observation for each station in the response. It does not guarantee that the source's most recent possible report has already arrived, that no correction will follow or that every station updates on the same schedule. A reportTime later than observedAt is preserved as separate source metadata. The Actor does not rewrite observation time to make an old measurement look newer.

At the source cap of four hundred records, the Actor marks possible upstream truncation. It cannot count records that were not returned. A missing station in a capped or malformed batch gets incomplete\_batch rather than a false clean absence. For a smaller trustworthy response, a requested station with no report can be recorded as not\_found. That distinction protects a consumer from treating omitted coverage as an observation of zero availability.

### R9. Decision routing

| Outcome | Suggested route | What it establishes |
|---|---|---|
| found=true, status=ok, stale=false | Current observation table | Complete source record passed the selected age threshold |
| status=stale | Historical/reference display or refresh queue | An observation exists but is too old for this result contract |
| status=not\_found, error empty | No-data state | Successful source request supplied no matching observation |
| status=source\_error | Retry/incident queue | Access or format failure; absence is not established |
| status=incomplete\_batch | Coverage review | Missing station cannot be classified from this response |
| status=partial | Batch review | Some observations or source coverage were left out |
| OUTPUT.fatal nonempty | Failed-run reconciliation | Input, money, delivery or all-content source failure |

A dashboard should display the observation time beside values. If a row is stale, either show a clearly labelled older value or show a missing-current-value state according to your own policy. Do not silently replace stale with not\_found; a known old measurement and no report are different evidence. Similarly, a source timeout should not look like a benign station outage when the whole endpoint was unreachable.

When a batch is partial but includes fresh observations, import those observations with the run's partial marker or a linked coverage record. A downstream comparison that requires every selected station should wait or report incomplete coverage. A comparison that can tolerate missing stations should state which ones were actually represented. The Actor does not decide that business rule for you.

When maxObservations is reached in history mode, inspect the time distribution before continuing. Newest-first selection can leave older periods uncovered and does not promise equal numbers per station. If balanced sampling is required, run smaller explicit station sets or implement a documented sampling step after retrieval. Do not claim balance simply because stationIds contained the same number of identifiers for each region.

When the source fails, bounded retry may be appropriate. Repeated 429 responses suggest your overall scheduling load needs attention. Multiple independent runs share the source's access limits even though each run individually follows its own rate cap. An external loop that immediately restarts failed runs can defeat the intended restraint and repeatedly incur start charges without improving coverage.

### R10. Commercial playbooks

**Facility weather overview.** A logistics analyst selects a small known station list and requests latest reports on a cadence that fits the team's review process. The dashboard displays stationId, observedAt, temperature, wind, visibility and a freshness label. A human uses the source report as context alongside the organization's operational systems. The dashboard should link or expose rawMetar so an unexpected decoded value can be checked without guessing what the source contained.

The analyst records run status and the effective input alongside each refresh. If one station has no data, the interface shows a no-report state for that station. If the batch fails, it shows a source problem rather than replacing every cell with zero. If a prior value remains on screen during an outage, its original observedAt stays visible. This prevents a failed refresh from making an old value appear current.

**Event-window analysis.** A data team investigates conditions around a known event using hours and maxAgeMinutes that cover the intended interval. It retains station-time identities and raw reports, imports the returned observations and checks partial notices before calculating summaries. The source window is at most twenty-four hours in this Actor. A retrospective beyond that window requires a different approved retrieval scope rather than an undocumented parameter hack.

For a chart of wind or temperature over time, deduplicate stationId plus observedAt and keep a separate correction/retrieval history if needed. A repeated run may return the same source observation, so counting rows across downloaded datasets without deduplication can exaggerate the sample size. If you preserve snapshots for audit, distinguish source observation count from retrieval count in the chart and its accompanying method note.

**Data-quality review.** An engineering team can use explicit nulls, qualifiers and source-error rows to test its ingestion pipeline. It checks that VRB remains a variable direction, that "10+" remains qualified visibility and that missing pressure is not converted to zero. These are useful contract checks for a weather table. They do not validate the physical instrument or certify the meteorological accuracy of the source report.

For each workflow, define what constitutes enough evidence before automating an action. A current overview may need one fresh observation per selected station; an event analysis may need several timestamps and no truncation; a data-quality review may deliberately inspect stale or missing values. The same dataset can serve these purposes only if the consumer preserves status, age and scope rather than reducing every row to a handful of numeric columns.

### R11. Integration recipes

**SQL time-series import.** Use stationId and observedAt as a natural observation key, or use observationId directly. Store checkedAt and the external Apify run ID as retrieval metadata. A latest table can upsert by stationId, but the update condition should compare observedAt so a delayed import does not overwrite a newer observation with an older one. Keep a history table if repeated retrievals or corrections matter to your workflow.

Store nullable measurements as nullable numeric fields. Keep visibilitySm in a representation that supports both numeric values and qualified strings; a separate normalized analysis column is acceptable if the original value is retained. Store clouds as JSON or a child table with layer order, cover and base. Do not force every observation into one cloud layer or fill missing values with zero merely to satisfy a rigid schema.

**Workflow engine.** Start an Actor run with the four-field JSON input, wait for terminal status, retrieve OUTPUT and the dataset, then route by fatal and partial. Filter found=true for the current-observation path. Keep stale and source-error rows in a separate operations path. Mark an import complete only after the destination accepts the data; a successful Actor run alone does not prove that the downstream database was updated.

A completion webhook is a notification about a run, not necessarily the observation payload. Use its run identifier to retrieve the authorized dataset and summary. Make notification processing idempotent so a duplicate webhook does not create a duplicate import or another paid Actor run. Store API tokens in the workflow's credential store, never in stationIds, the output dataset or a spreadsheet shared for review.

**Python analysis.** Parse the JSON export, filter the intended statuses and convert observedAt with a timezone-aware parser. Group observations by station before calculating time differences or trends. Handle null explicitly and separate stale rows from the paid current set. For visibility analysis, parse qualifiers into a value-plus-bound representation instead of casting every string to a float and discarding the plus sign.

**JavaScript dashboard.** Use observedAt to compute the displayed age at render time, while preserving ageMinutes as the Actor's processing-time evidence. Render rawMetar as text. Treat all source strings as data rather than HTML or code. If the user selects a local display timezone, keep the UTC value in the stored object and label the display timezone. Weather comparisons become confusing quickly when one table silently mixes local and UTC times.

**Spreadsheet review.** Import a properly generated CSV and keep stationId as text. Use a date-time format that includes timezone context. Qualifier strings should not be coerced to numbers, and empty numeric cells should remain missing. A report's embedded spaces are part of rawMetar and should occupy one cell after CSV parsing. Retain status and stale columns so a reviewer can tell which numeric values belong to an old report.

**Alerting integration.** Define your own thresholds and required freshness in the consumer, then inspect the source and partial state before emitting an alert. This Actor does not send alerts or decide operational safety. Keep derived flags separate from raw measurements and record the rule version used. A threshold crossing based on a stale or qualified measurement may require different treatment from a fresh exact measurement, depending on your rule.

### R12. Operating guide

Choose an observation cadence before enabling a schedule. Most stations do not create a new report every second, and repeated retrieval can return the same station-time identity. A schedule should fit the actual use of the data, the source's documented restrictions and your willingness to pay for new starts and repeated observations. The Actor's per-run rate limit does not coordinate every independent workflow in your account.

Begin with one or two stations and latest mode. Verify expected station identities, raw report prefix, units and age behavior. Then add the remaining stations, inspect coverage and decide whether a history window is necessary. Changing station set, hours and age threshold simultaneously makes it harder to identify why a result count changed. Preserve a baseline input to compare after future releases or source changes.

Save the effective input, Actor build/version, run ID, terminal status and OUTPUT in your own batch ledger. Source rows carry observedAt and checkedAt, but not every operational detail of the platform run. Those two layers together support a reproducible explanation: which stations and period were requested, which source reports were returned, and whether delivery or billing failed after part of the dataset was written.

If reports are unexpectedly stale, inspect observedAt first. A fresh reportTime alone does not make an older observation fresh. Confirm that maxAgeMinutes matches the intended history window and that your display has not confused seconds with milliseconds or local time with UTC. Avoid changing the timestamp to suppress a stale label; the timestamp is evidence that should remain intact.

If the response reaches four hundred records, reduce the scope if you need a more reliable complete subset. A shorter history window or smaller station set is easier to assess. The Actor does not paginate beyond the source cap and does not substitute a worldwide cache. A partial flag at the upstream cap is a conservative coverage statement, not a measured count of exactly how many reports were lost.

If a response is malformed, retain the error row and the run input. The Actor does not bill a corrupt observation or parse an oversized response prefix as a complete result. A format repair should be tested against the affected shape and the existing negative, freshness and money tests. Keep the source endpoint and schema version visible so a change in provenance is not mistaken for a harmless field rename.

If a write or charge fails, inspect the actual dataset and platform chargedEventCounts before retrying. The linked SDK operation writes before charging, so an exception can mean a valid row already exists without a confirmed charge acknowledgement. The Actor records deliveryUncertain and fails. Your importer should avoid treating that failed run as either definitely empty or fully successful, and should upsert repeated station-time identities safely.

### R13. FAQ

**Is this a forecast service?** No. It extracts METAR observations and selected decoded fields from the official source. It does not request TAF, predict future conditions or calculate a risk score. Keep the distinction clear in any product that consumes the dataset, especially when a chart contains observations alongside forecasts from another source.

**Can I use city names or IATA codes?** The input requires four-character station identifiers. It does not resolve city names, addresses or three-character airport codes. Perform any station selection in your own workflow and preserve the mapping you used. A correctly shaped identifier can still have no report available from the source at the requested time.

**Does hours=24 mean every report for every station is guaranteed?** No. The source and Actor have record, byte, time and result caps. maxAgeMinutes can also make older reports stale. Inspect the partial state and the coverage counters before calling the result complete. The Actor does not fill gaps or invent regularly spaced hourly samples.

**Why is a historical report stale when I requested history?** hours controls what the source is asked to return; maxAgeMinutes controls which returned reports meet your paid freshness contract. Set both intentionally. A 24-hour window with a 180-minute age threshold can legitimately produce many free stale rows and only a few fresh observations.

**Why is visibility a string?** The source can qualify a visibility value, such as "10+". The Actor preserves that information. Converting it into exactly 10 would remove the qualifier and could distort a calculation. Keep the source value and make any analysis-specific conversion explicit in a separate field.

**Why is windDirectionDeg null when wind is present?** Variable wind can be represented by VRB rather than a single degree value. windDirectionRaw preserves that source value while windDirectionDeg remains null. Missing direction is also possible. Neither case justifies replacing the value with north or zero degrees.

**Can reportTime be later than observedAt?** Yes; they are different source concepts. Freshness is calculated from the actual observation time. The Actor preserves the report time for provenance and does not use it to make the observation younger. More than five minutes of future observation time is treated as an inconsistent source record.

**Why is a stale row free even though it contains measurements?** The paid unit is a complete fresh observation under your selected age threshold. A stale row carries evidence so you can understand what the source returned, but it does not satisfy that freshness contract and receives no result event. The automatic start charge can still apply.

**Why is a missing station labelled incomplete\_batch?** The response may have reached the upstream record cap or contained malformed data, so the Actor cannot conclude that the station had no report. A clean not\_found statement requires a trustworthy successful response. Incomplete coverage and confirmed absence are intentionally separate statuses.

**Do two stations always produce two results?** No. One may have no report, stale data or a malformed record, or your shared maxObservations can be one. In history mode, a station may contribute several observations. The number of station IDs is a request count, not a guaranteed result count or billing count.

**Does maxObservations include free rows?** No. It limits complete fresh observations across the batch. Free stale, absence, error and partial rows can make the dataset longer. Filter found=true for paid content and retain the remaining rows when assessing coverage or explaining a low result count.

**Is deduplication persistent across runs?** No. The same station-time identity is emitted at most once within one run. A later run can retrieve and bill it again. Keep a persistent observation key in your own database if you want idempotent imports, and choose a schedule that does useful work rather than assuming the Actor provides a free cache.

**Can it be used as an official flight briefing?** This extraction product provides a bounded evidence table and is not an official briefing or operational authorization. It does not gather every aviation product or assess route conditions. Use the appropriate official services and your established operational process for those decisions.

**How should I report a defect?** Provide the Actor run ID, effective input, terminal status, relevant OUTPUT fields and a redacted source/error row through the platform issue channel when available. Avoid including tokens or unrelated personal information. A concrete station and observation timestamp make a report more reproducible than a screenshot of an unlabeled chart.

### R14. Sources and rights

The [AWC Data API documentation](https://aviationweather.gov/data/api/) identifies the machine-to-machine route and operating restrictions. Its [OpenAPI schema](https://aviationweather.gov/data/schema/openapi.yaml) describes the METAR JSON fields and their units. This Actor uses that documented route with a bounded station list and frequency. It does not interpret a missing robots file as permission for unrelated paths or unrestricted bulk access.

NOAA, the National Weather Service and the Aviation Weather Center identify the source organizations. This Actor is an independent data extraction product and does not imply endorsement or certification by those organizations. Preserve source provenance in downstream displays and review the source's current notices for your intended redistribution or application. A derived dashboard or analysis remains your own product, with its own method and limitations.

The examples above are accepted-run evidence and the tariff was read back from the listing. No placeholder image is presented here as a screenshot of a live listing, and no public scheduled task is claimed to exist.

#### Related tools

Related tools for adjacent workflows in official open-data APIs.

| Actor | What it does |
|---|---|
| [Clinical Trials Monitor](https://apify.com/zinin/clinical-trials-monitor) | Pair it in the official open-data APIs workflow: Retrieve up to 100 ClinicalTrials.gov studies for up to 25 condition, drug, or sponsor queries |
| [FDA Approval Feed](https://apify.com/zinin/fda-approval-feed) | Pair it in the official open-data APIs workflow: Collect bounded openFDA approval and recall observations by drug or company |
| [Gov Tender Radar](https://apify.com/zinin/gov-tender-radar) | Pair it in the official open-data APIs workflow: Search official TED procurement notices by keyword, CPV query, or buyer country |
| [Patent Evidence Normalizer](https://apify.com/zinin/patent-monitor) | Pair it in the official open-data APIs workflow: Normalize buyer-owned, rights-holder-authorized, PatentsView CC BY 4.0, or licensed patent exports into... |
| [Sanctions Screening API](https://apify.com/zinin/sanctions-screening) | Pair it in the official open-data APIs workflow: Screen up to 100 names against fresh official OFAC SDN primary and alias files plus the EU consolidated... |

# Actor input Schema

## `stationIds` (type: `array`):

1–20 four-character ICAO station identifiers. Lowercase is normalized and duplicates removed. One bounded batch request; no airport-name search.

## `hours` (type: `integer`):

0 requests the latest report per station. 1–24 requests recent history within that window. maxAgeMinutes still applies to every report.

## `maxObservations` (type: `integer`):

Total complete fresh observations delivered across the batch, newest first. Stale/error/absence/partial rows are free and additional.

## `maxAgeMinutes` (type: `integer`):

Reports older than this threshold are marked stale and not billed as results. Compare against observedAt, not the later reportTime.

## Actor input object example

```json
{
  "stationIds": [
    "KJFK",
    "EGLL"
  ],
  "hours": 0,
  "maxObservations": 20,
  "maxAgeMinutes": 180
}
```

# Actor output Schema

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

No description

## `summary` (type: `string`):

No description

# 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 = {
    "stationIds": [
        "KJFK",
        "EGLL"
    ],
    "hours": 0,
    "maxObservations": 20,
    "maxAgeMinutes": 180
};

// Run the Actor and wait for it to finish
const run = await client.actor("zinin/noaa-metar-weather-extractor").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 = {
    "stationIds": [
        "KJFK",
        "EGLL",
    ],
    "hours": 0,
    "maxObservations": 20,
    "maxAgeMinutes": 180,
}

# Run the Actor and wait for it to finish
run = client.actor("zinin/noaa-metar-weather-extractor").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 '{
  "stationIds": [
    "KJFK",
    "EGLL"
  ],
  "hours": 0,
  "maxObservations": 20,
  "maxAgeMinutes": 180
}' |
apify call zinin/noaa-metar-weather-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,zinin/noaa-metar-weather-extractor"
        }
    }
}

```

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/QucWlK54osIQhjqa0/builds/GAl6YWBhkFqRnOjNZ/openapi.json
