API Documentation

  • ✨ API Version 2 — Expanded station support (30 stations), metric units for international locations, Wethr Low calculations, and explicit logic parameters. Base URL: https://wethr.net/api/v2/

    Introduction

    The Wethr.net API v2 provides programmatic access to weather observation data and model forecasts. It includes support for 30 weather stations across the US and international markets, with native metric units for non-US stations.

    Base URL: https://wethr.net/api/v2/


    What's New in v2

    • Data Quality Warnings NEW: The wethr_high mode and the Push observation event now include a data_quality object that flags missing temperature readings (the -999 sentinel) in the trading-day window. It is keyed per resolution mode and escalates to critical when the station is currently down or a gap falls near the reported high/low. See Observations Data Quality and the Push Data Quality Flags.
    • Daily Summary Mode for Forecasts NEW: The Forecasts API now accepts mode=daily, returning a per-model, per-day high/low summary bucketed by the station's local time. A tz_mode parameter selects the day-boundary convention (civil for WU/Polymarket resolution, standard for NWS/Kalshi). Only fully-covered days are returned; pin a historical run via run for backtesting.
    • Latest Run Mode for Forecasts NEW: The Forecasts API now accepts a run parameter (latest or ISO 8601 timestamp). Use run=latest&model=HRRR to retrieve every forecast hour of the most recent run without needing to specify a time window — ideal for real-time model lookups. Responses include an X-Run-Time header identifying the resolved run cycle.
    • Push API BETA: Stream real-time observations, DSM/CLI releases, and temperature extreme alerts
    • Model Accuracy API NEW DEV+: Per-model forecast accuracy metrics (MAE, bias, RMSE) across 66 stations with filtering by model, run time, lead time, and time window. Supports both daily highs and daily lows. Requires Developer or Enterprise tier.
    • Nearby Stations API NEW BETA DEV+: Find weather stations near a target station (by radius) or look up an arbitrary list of stations, each with its latest observation. Useful for cross-referencing readings, building heat maps, or detecting microclimates. Requires Developer or Enterprise tier.
    • Pacing API NEW BETA DEV+: See how the latest actual observation is running versus each model's forecast for the contract day, with the model temperature interpolated to the observation's exact time. Returns a low/high pace per model (plus an NWS row) in the station's native units — the same calculation shown on the dashboard. Requires Developer or Enterprise tier.
    • Precipitation API BETA: New endpoint for monthly precipitation totals with real-time ASOS data
    • NWS Forecasts API BETA: New endpoint for NWS hourly temperature forecasts with version history
    • Expanded Station Support: 30 stations including London, Buenos Aires, Toronto, and Seoul
    • Metric Units: International stations (EGLC, SAEZ, CYYZ, RKSI, NZWN, SBGR, LFPB, LTAC, VILK, EDDM, LLBG, RJTT, ZSPD, WSSS, EPWA, LIMC, LEMD, ZUUU, ZBAA, ZHHH, ZUCK, ZGSZ, CYUL, CYHC, LFPO, YSSY, UUWW, RCSS, LTFM, MMMX, VHKO, EFHK, EHAM, RKPK, WIHH, WMKK, MPMG, DNMM, OEJN, FACT, OPKC, ZGGG, RPLL, ZSQD) return temperatures in Celsius
    • Wethr Low: The wethr_high mode now also returns wethr_low
    • Improved Wethr High/Low Accuracy (NWS mode): NWS logic incorporates high-frequency observations to capture extremes between standard METAR reporting intervals. WU/Polymarket logic uses only standard METAR observations.
    • Required Logic Parameter: Explicit logic parameter required for wethr_high mode (nws or wu)
    • Observation Type Filter: Filter latest mode by dsm_high, dsm_low, cli_high, cli_low
    • Units Indicator: All responses include a units field
    • One Minute Observations (OMO) COMING SOON: Raw One Minute Observation data is not yet available via the API (REST or Push). We anticipate making OMO data available in the coming months. However, Wethr High and Wethr Low values already incorporate OMO data and update in real time when a one minute observation impacts the calculated high or low.

    Authentication

    All requests require an API key. Include it in the HTTP header:

    Authorization: Bearer <YOUR_API_KEY>

    Or use the custom header:

    X-API-Key: <YOUR_API_KEY>

    Generate API keys in your Account Settings.


    Rate Limiting

    TierRequests/MinuteRequests/Day
    Professional605,000
    Developer300100,000
    Enterprise1200500,000

    Supported Stations

    US Stations (Fahrenheit)

    CodeLocationTimezone
    KMDWChicago MidwayAmerica/Chicago
    KPHLPhiladelphiaAmerica/New_York
    KMIAMiamiAmerica/New_York
    KLAXLos AngelesAmerica/Los_Angeles
    KBKFAurora (Buckley SFB)America/Denver
    KDENDenverAmerica/Denver
    KAUSAustinAmerica/Chicago
    KNYCNew York (Central Park)America/New_York
    KDFWDallas/Fort WorthAmerica/Chicago
    KMSYNew OrleansAmerica/Chicago
    KLGANew York LaGuardiaAmerica/New_York
    KDALDallas Love FieldAmerica/Chicago
    KHOUHouston HobbyAmerica/Chicago
    KSEASeattleAmerica/Los_Angeles
    KSFOSan FranciscoAmerica/Los_Angeles
    KLASLas VegasAmerica/Los_Angeles
    KPHXPhoenixAmerica/Phoenix
    KSATSan AntonioAmerica/Chicago
    KDCAWashington D.C.America/New_York
    KCLTCharlotteAmerica/New_York
    KBOSBostonAmerica/New_York
    KBNANashvilleAmerica/Chicago
    KATLAtlantaAmerica/New_York
    KJAXJacksonvilleAmerica/New_York
    KOKCOklahoma CityAmerica/Chicago
    KDTWDetroitAmerica/Detroit
    KMSPMinneapolisAmerica/Chicago

    International Stations (Celsius) NEW

    CodeLocationTimezone
    EGLCLondon City AirportEurope/London
    SAEZBuenos Aires EzeizaAmerica/Argentina/Buenos_Aires
    CYYZToronto PearsonAmerica/Toronto
    RKSISeoul IncheonAsia/Seoul
    SBGRSão Paulo GuarulhosAmerica/Sao_Paulo
    LFPBParis Le BourgetEurope/Paris
    LTACEsenboğa International AirportEurope/Istanbul
    VILKChaudhary Charan Singh International AirportAsia/Kolkata
    EDDMMunich AirportEurope/Berlin
    LLBGBen Gurion International AirportAsia/Jerusalem
    RJTTTokyo Haneda AirportAsia/Tokyo
    ZSPDShanghai Pudong AirportAsia/Shanghai
    WSSSSingapore Changi AirportAsia/Singapore
    EPWAWarsaw Chopin AirportEurope/Warsaw
    LIMCMilan Malpensa AirportEurope/Rome
    LEMDMadrid Barajas AirportEurope/Madrid
    ZUUUChengdu Shuangliu AirportAsia/Shanghai
    ZBAABeijing Capital AirportAsia/Shanghai
    ZHHHWuhan Tianhe AirportAsia/Shanghai
    ZUCKChongqing Jiangbei AirportAsia/Shanghai
    ZGSZShenzhen Bao'an AirportAsia/Shanghai
    CYULMontreal Trudeau AirportAmerica/Toronto
    CYHCVancouver HarbourAmerica/Vancouver
    LFPOParis Orly AirportEurope/Paris
    YSSYSydney Kingsford Smith AirportAustralia/Sydney
    UUWWMoscow Vnukovo AirportEurope/Moscow
    RCSSTaipei Songshan AirportAsia/Taipei
    LTFMIstanbul AirportEurope/Istanbul
    MMMXMexico City International AirportAmerica/Mexico_City
    VHKOHong Kong ObservatoryAsia/Hong_Kong
    EFHKHelsinki-Vantaa AirportEurope/Helsinki
    EHAMAmsterdam Schiphol AirportEurope/Amsterdam
    RKPKGimhae International AirportAsia/Seoul
    WIHHHalim Perdanakusuma AirportAsia/Jakarta
    WMKKKuala Lumpur International AirportAsia/Kuala_Lumpur
    MPMGMarcos Gelabert AirportAmerica/Panama
    DNMMLagos/Ikeja AirportAfrica/Lagos
    OEJNJeddah/King Abdulaziz AirportAsia/Riyadh
    FACTCape Town/DF Malan AirportAfrica/Johannesburg
    OPKCKarachi International AirportAsia/Karachi
    ZGGGGuangzhou/Baiyun AirportAsia/Shanghai
    RPLLNinoy Aquino International AirportAsia/Manila
    ZSQDQingdao/Tsingtao AirportAsia/Shanghai

    Error Responses

    • 400 Bad Request — Invalid or missing parameters
    • 401 Unauthorized — Invalid or missing API key
    • 403 Forbidden — Tier restriction (e.g., Model Accuracy API requires Developer or Enterprise)
    • 429 Too Many Requests — Rate limit exceeded
    • 500 Server Error — Internal server error
    {
        "error": "Missing required parameter: logic",
        "details": "The 'logic' parameter is required for wethr_high mode. Valid values: 'nws' or 'wu'."
    }

    Observations API

    GET /api/v2/observations.php
    Note — One Minute Observations (OMO): Raw OMO data is not currently available through the Observations REST API. We anticipate adding support for One Minute Observations in the coming months. Currently, only standard METAR, HF-METAR, and SPECI observations are returned. However, Wethr High and Wethr Low values (mode=wethr_high) already incorporate OMO data and update in real time when a one minute observation impacts the calculated high or low.

    Mode: Latest Observation

    Returns the most recent observation for a station.

    Parameters

    ParameterRequiredDescription
    station_codeYesStation identifier
    modeYesSet to latest
    observation_typeNoFilter: dsm_high, dsm_low, cli_high, cli_low

    Example Request

    GET /api/v2/observations.php?station_code=KMDW&mode=latest
    GET /api/v2/observations.php?station_code=KMDW&mode=latest&observation_type=cli_high

    Example Response

    {
        "station_code": "KMDW",
        "observation_time": "2025-06-15 18:53:00",
        "temperature": 26.7,
        "temperature_display": 80.1,
        "lowest_probable": 80,
        "highest_probable": 80,
        "units": "fahrenheit",
        "dsm_high": 28.3,
        "dsm_high_display": 83,
        "relative_humidity": 55.2,
        "altimeter": 1019.8,
        "sea_level_pressure": 1019.6
    }

    Mode: Wethr High/Low Calculation

    Calculates the current trading-day high and low temperatures based on multiple observation sources.

    • NWS logic: Uses CLI/DSM reports, 6-hour highs/lows, and all available observations to capture extremes between standard reporting intervals. Time window follows Standard Time year-round.
    • WU logic: Uses only standard METAR observations. Time window follows Local Time year-round. See Precision Validation for US Stations for details on how observation accuracy is ensured.

    Parameters

    ParameterRequiredDescription
    station_codeYesStation identifier
    modeYesSet to wethr_high
    logic Yes nws — NWS/Kalshi: Standard Time year-round, uses CLI/DSM/6hr data
    wu — Weather Underground: Local Time year-round, uses exact temperature

    Example Request

    GET /api/v2/observations.php?station_code=KMDW&mode=wethr_high&logic=nws

    Example Response

    {
        "station_code": "KMDW",
        "date": "2025-06-15",
        "wethr_high": 83,
        "wethr_low": 68,
        "time_of_high_utc": "2025-06-15 20:53:00",
        "time_of_low_utc": "2025-06-15 10:53:00",
        "calculation_logic": "nws",
        "units": "fahrenheit",
        "data_quality": {
            "nws": {"ok": true, "warnings": []}
        }
    }

    Data Quality NEW

    The wethr_high response includes a data_quality object that flags missing temperature readings (the -999 sentinel the station logs when a sensor does not report) within the trading-day window. Missing readings are simply absent from the high/low calculation, so a gap can leave the reported wethr_high/wethr_low understated — this object tells you when coverage was incomplete.

    It is keyed by the logic you requested (nws or wu), since each mode scans its own time window. When the day's observations are complete, ok is true and warnings is empty.

    {
        "data_quality": {
            "nws": {
                "ok": false,
                "warnings": [
                    {
                        "code": "temperature_data_missing",
                        "severity": "critical",
                        "message": "2 temperature readings missing (-999) in this window; longest gap 2 consecutive (~10 min) — a gap falls within ~60 min of the day's high, so wethr_high/wethr_low may be unreliable.",
                        "missing_count": 2,
                        "longest_run": 2,
                        "longest_run_minutes": 10,
                        "first_missing_utc": "2025-06-15 20:43:00",
                        "last_missing_utc": "2025-06-15 20:48:00",
                        "active": false,
                        "active_run": 0,
                        "near_extreme": true,
                        "near_high": true,
                        "near_low": false
                    }
                ]
            }
        }
    }

    The temperature_data_missing warning object and its severity rules are identical to the one delivered through the Push API. See the Push Data Quality Flags section for the full field reference.

    WU Logic: Precision Validation for US Stations

    METAR observations report temperature in two ways: the body of the report contains a whole-degree Celsius value (e.g., 26/08), while the remarks section may contain a "T-group" with tenths-of-a-degree precision (e.g., T02560078 = 25.6°C). The difference matters because 26°C rounds to 79°F, while 25.6°C rounds to 78°F — a 1°F discrepancy from a single observation that can affect the Wethr High.

    US ASOS stations (ICAO codes beginning with K) nearly always produce T-groups. However, when a new METAR is first published, the body temperature may be available before the complete remarks section has propagated through all data sources. During this brief window, the observation may appear to have a rounded whole-degree temperature when a more precise value is forthcoming.

    To prevent this from affecting the Wethr High/Low calculation, the API applies the following validation when using logic=wu:

    • If a K-station observation has tenths-of-a-degree precision (T-group present), it is used immediately — the temperature is exact and unambiguous.
    • If a K-station observation has only whole-degree precision (no T-group), the API requires confirmation from a secondary data source before including it in the Wethr High/Low calculation. This ensures the T-group has had time to propagate and that the rounded value is not premature.

    This validation applies only to WU logic. NWS logic is unaffected because it uses temperature ranges (lowest_probable / highest_probable) rather than a single rounded value, which inherently accounts for rounding uncertainty.


    Mode: History

    Retrieves observations over a time range (max 24 hours). Default mode when mode is omitted.

    Parameters

    ParameterRequiredDescription
    station_codeYesStation identifier
    start_timeYesStart of range (ISO 8601 UTC)
    end_timeYesEnd of range (ISO 8601 UTC)

    Example Request

    GET /api/v2/observations.php?station_code=KMDW&start_time=2025-06-15T00:00:00Z&end_time=2025-06-15T12:00:00Z

    Observation Data Model

    FieldTypeDescription
    idintegerUnique record identifier
    station_codestringStation identifier
    observation_timedatetimeUTC timestamp
    temperaturedecimalTemperature in Celsius (raw)
    temperature_displaydecimalTemperature in station's native units
    lowest_probableintegerMin probable temp (native units)
    highest_probableintegerMax probable temp (native units)
    unitsstringfahrenheit or celsius
    precision_levelintegerData precision (1 = exact)
    dew_pointdecimalDew point (Celsius)
    relative_humiditydecimalRelative humidity (%)
    wind_directionstringWind direction
    wind_speeddecimalWind speed (knots)
    wind_gustdecimalWind gust (knots)
    visibilitydecimalVisibility (statute miles)
    altimeterdecimalAltimeter setting in hectopascals (hPa). Divide by 33.8639 to convert to inches of mercury (inHg).
    sea_level_pressuredecimalSea level pressure in hectopascals (hPa). Reported in METAR remarks when available; may be null for observations without an SLP group.
    six_hour_highdecimal6-hour high (Celsius)
    six_hour_lowdecimal6-hour low (Celsius)
    cli_high / cli_lowdecimalCLI report values (US only)
    dsm_high / dsm_lowdecimalDSM report values (US only)
    *_displayintegerValues in station's native units
    *_fintegerValues in Fahrenheit (backward compat)

    Client Examples

    cURL — Wethr High with NWS Logic

    curl -G "https://wethr.net/api/v2/observations.php" \
      -H "Authorization: Bearer <YOUR_API_KEY>" \
      --data-urlencode "station_code=KMDW" \
      --data-urlencode "mode=wethr_high" \
      --data-urlencode "logic=nws"

    cURL — Latest CLI High

    curl -G "https://wethr.net/api/v2/observations.php" \
      -H "Authorization: Bearer <YOUR_API_KEY>" \
      --data-urlencode "station_code=KMDW" \
      --data-urlencode "mode=latest" \
      --data-urlencode "observation_type=cli_high"

    JavaScript — Wethr High/Low

    const params = new URLSearchParams({
        station_code: 'KMDW',
        mode: 'wethr_high',
        logic: 'nws'
    });
    
    fetch(`/api/v2/observations.php?${params}`, {
        headers: { 'Authorization': `Bearer ${apiKey}` }
    })
    .then(res => res.json())
    .then(data => {
        console.log(`High: ${data.wethr_high}° / Low: ${data.wethr_low}°`);
    });

    Python — Latest Observation

    import requests
    
    response = requests.get(
        'https://wethr.net/api/v2/observations.php',
        params={'station_code': 'KMDW', 'mode': 'latest'},
        headers={'Authorization': f'Bearer {api_key}'}
    )
    data = response.json()
    print(f"Current: {data['temperature_display']}°{data['units'][0].upper()}")

    Forecasts API

    GET /api/v2/forecasts.php

    Retrieves model forecast data for a location within a time range.

    Parameters

    ParameterRequiredDescription
    location_nameYesLocation identifier (e.g., KMDW)
    start_valid_timeConditionalStart of forecast range (ISO 8601). Required unless a run filter is specified.
    end_valid_timeConditionalEnd of forecast range (ISO 8601). Required unless a run filter is specified.
    model Conditional Filter by model: ARPEGE, ECMWF-IFS, GEFS, GEM-GDPS, GEM-HRDPS, GFS, GFS-MOS, HRRR, ICON, JMA, LAV-MOS, NAM, NAM-MOS, NAM4KM, NBM, NBS-MOS, RAP, UKMO. Optional in normal mode; required when run=latest.
    run NEW No Filter results to a single model run. Accepts:
    • latest — resolves to the most recent run for the specified model + location_name. When used, model becomes required.
    • An ISO 8601 timestamp matching a specific run_time (e.g., 2026-04-20T13:00:00Z).
    When any run filter is specified, the start_valid_time / end_valid_time window becomes optional (omit to receive every forecast hour the run produced; supply it to filter the run's output to a subset). Omit the parameter entirely for default behavior (returns all runs intersecting the requested window).
    mode NEW No Set to daily to receive a per-model, per-day high/low summary instead of raw hourly rows. Days are bucketed by the station's local time, and a day is returned only when the model run covers it completely. See Daily Summary Mode. Omit for default (raw hourly) output.
    tz_mode NEW No Day-boundary convention for mode=daily. Ignored in all other modes. Accepts:
    • civil (default) — local wall-clock time, adjusting for DST. Day boundaries follow the local civil day (23, 24, or 25 hours across DST transitions). Matches WU / Polymarket resolution.
    • standard — Local Standard Time year-round, no DST adjustment. Every day is exactly 24 hours. Matches NWS climate reporting and Kalshi temperature market resolution.

    Example Request

    GET /api/v2/forecasts.php?location_name=KMDW&start_valid_time=2025-06-15T18:00:00Z&end_valid_time=2025-06-15T20:00:00Z&model=HRRR

    Latest Run Mode NEW

    Passing run=latest returns every forecast hour of the most recent run for a given model and location_name, without requiring the caller to know the model's run schedule or forecast horizon. This is the recommended way to retrieve "the latest HRRR for KAUS" and similar real-time lookups.

    Behavior:

    • model is required. This prevents ambiguity across models that update on different cadences (e.g., HRRR hourly vs. GFS every 6 hours).
    • start_valid_time and end_valid_time are optional. Omit them to receive every forecast hour the run produced (F00 through the model's maximum lead time). Supply them to filter the run's output to a specific window (e.g., "latest HRRR, but only the next 6 hours").
    • The response includes an X-Run-Time header containing the resolved run's initialization time (UTC), so clients can confirm which cycle they received without parsing rows.
    • If no runs exist for the specified model/location, the endpoint returns an empty array ([]) with HTTP 200.

    Example Request — Latest HRRR Run (All Forecast Hours)

    GET /api/v2/forecasts.php?location_name=KAUS&model=HRRR&run=latest

    Example Request — Specific Run by Timestamp

    Retrieve a specific historical run by passing an ISO 8601 timestamp to run. The window is optional; omit it to get every forecast hour the run produced:

    GET /api/v2/forecasts.php?location_name=KAUS&model=HRRR&run=2026-04-20T13:00:00Z

    Daily Summary Mode NEW

    Passing mode=daily collapses the hourly forecast into a per-model, per-day high and low, bucketed by the station's local time. Instead of one row per forecast hour, you get one row per model per day. This is intended for daily temperature brackets and model backtesting, where the unit of interest is the day's extreme rather than the hourly trace.

    Behavior:

    • Local-time days. Each forecast hour (stored in UTC) is converted to the station's local time using its IANA timezone, then grouped by local calendar day. The day boundary convention is controlled by tz_mode (civil, the default, or standard).
    • Complete days only. A day is returned only when the model run fully covers it — that is, the run's forecast hours span the entire local day from midnight to midnight. Partial days at the start of a run (which begins mid-day) and at the end of its forecast horizon are omitted. A single run that fully covers several days returns one row per covered day.
    • Run selection. By default, each model is summarized at its latest run, so model is optional (omit it to summarize every available model; supply it to restrict to one). To summarize a specific historical run instead — for example, when backtesting — pass that run's timestamp via run (e.g., run=2026-06-01T12:00:00Z); the summary then reflects only that run.
    • All models included. No model is filtered out. Gridded models (e.g., GFS, HRRR, NAM4KM) and statistical guidance (e.g., GFS-MOS, NBS-MOS) are summarized alike. Because guidance products are often issued at coarser intervals, their high/low is derived from fewer points; the points_used field reports how many forecast points contributed to each day so you can account for sampling resolution.
    • Response headers. An X-Run-Signature header identifies the set of runs used to build the summary, so the cached response rotates automatically when a newer run lands.
    • If no fully-covered day exists for the station (for example, when stored runs do not yet extend far enough), the endpoint returns an empty array ([]) with HTTP 200.
    • If the station's timezone is not configured in the catalog, the endpoint returns HTTP 422 rather than silently bucketing in UTC.

    Example Request — Daily High/Low, All Models

    GET /api/v2/forecasts.php?location_name=KMDW&mode=daily

    Example Request — Single Model, Standard-Time Days

    GET /api/v2/forecasts.php?location_name=KMDW&mode=daily&model=GFS&tz_mode=standard

    Daily Summary Fields

    FieldTypeDescription
    modelstringForecast model name
    datestringLocal calendar day (YYYY-MM-DD) in the station's time
    timezonestringIANA timezone used for bucketing (e.g., America/Chicago)
    tz_modestringDay-boundary convention applied: civil or standard
    run_timedatetimeModel run the summary was derived from (UTC)
    points_usedintegerNumber of forecast points that contributed to this day's high/low
    high_fdecimalDay's maximum temperature (Fahrenheit)
    low_fdecimalDay's minimum temperature (Fahrenheit)
    high_cdecimalDay's maximum temperature (Celsius)
    low_cdecimalDay's minimum temperature (Celsius)

    Note: Fahrenheit fields are present for US stations and Celsius fields for international stations, mirroring the native units of the underlying forecast data; a field is omitted for a day when the source model did not provide that unit.

    Example Response

    [
        {
            "model": "GFS",
            "date": "2026-06-02",
            "timezone": "America/Chicago",
            "tz_mode": "civil",
            "run_time": "2026-06-01 06:00:00",
            "points_used": 24,
            "high_f": 73.1,
            "low_f": 56.4,
            "high_c": 22.8,
            "low_c": 13.5
        },
        {
            "model": "NAM4KM",
            "date": "2026-06-02",
            "timezone": "America/Chicago",
            "tz_mode": "civil",
            "run_time": "2026-06-01 12:00:00",
            "points_used": 24,
            "high_f": 71.3,
            "low_f": 55.1,
            "high_c": 21.8,
            "low_c": 12.8
        }
    ]

    Tip: For backtesting which model is most accurate at a given lead time, pin a historical run with run and read the returned date values against that run's run_time: the gap between them is the lead time, and a single run covering multiple days lets you score day+1, day+2, and beyond from one response.

    Forecast Data Model

    FieldTypeDescription
    idintegerUnique record identifier
    modelstringForecast model name
    location_namestringLocation identifier
    latitudedecimalLatitude
    longitudedecimalLongitude
    run_timedatetimeModel run time (UTC)
    valid_timedatetimeForecast valid time (UTC)
    forecast_hourintegerLead time in hours
    temperature_kdecimalTemperature (Kelvin)
    temperature_fdecimalTemperature (Fahrenheit)
    temperature_cdecimalTemperature (Celsius)
    relative_humidity NEWdecimalRelative humidity (%)
    dew_point_f NEWdecimalDew point (Fahrenheit)
    dew_point_c NEWdecimalDew point (Celsius)
    apparent_temp_f NEWdecimalApparent ("feels like") temperature (Fahrenheit)
    apparent_temp_c NEWdecimalApparent ("feels like") temperature (Celsius)
    wind_speed_mph NEWdecimalWind speed (mph)
    wind_speed_kmh NEWdecimalWind speed (km/h)
    wind_direction_deg NEWintegerWind direction (degrees, 0–360)
    wind_gusts_mph NEWdecimalWind gusts (mph)
    wind_gusts_kmh NEWdecimalWind gusts (km/h)
    cloud_cover NEWdecimalTotal cloud cover (%)
    precipitation_mm NEWdecimalPrecipitation (millimeters)
    precipitation_in NEWdecimalPrecipitation (inches)
    weather_code NEWintegerWMO weather interpretation code
    pressure_msl_hpa NEWdecimalMean sea-level pressure (hPa)
    surface_pressure_hpa NEWdecimalSurface pressure (hPa)
    inserted_atdatetimeTimestamp when this forecast record was ingested into the database (UTC). Useful as a freshness indicator for weighting or staleness calculations.

    Note: The extended fields (humidity, dew point, apparent temperature, wind, cloud cover, precipitation, weather code, and pressure) are populated where the source model provides them. A field will be null for any model/run that does not supply that variable, so clients should treat all extended fields as nullable. The temperature fields are present for all models.

    Example Response

    [
        {
            "id": 123456,
            "model": "HRRR",
            "location_name": "KMDW",
            "latitude": 41.786,
            "longitude": -87.752,
            "run_time": "2025-06-15 18:00:00",
            "valid_time": "2025-06-15 19:00:00",
            "forecast_hour": 1,
            "temperature_k": 299.82,
            "temperature_f": 80.0,
            "temperature_c": 26.5,
            "relative_humidity": 55.0,
            "dew_point_f": 62.5,
            "dew_point_c": 16.9,
            "apparent_temp_f": 82.4,
            "apparent_temp_c": 28.0,
            "wind_speed_mph": 11.5,
            "wind_speed_kmh": 18.5,
            "wind_direction_deg": 210,
            "wind_gusts_mph": 18.2,
            "wind_gusts_kmh": 29.3,
            "cloud_cover": 40.0,
            "precipitation_mm": 0.0,
            "precipitation_in": 0.0,
            "weather_code": 2,
            "pressure_msl_hpa": 1014.2,
            "surface_pressure_hpa": 1009.8,
            "inserted_at": "2025-06-15 18:42:13"
        },
        {
            "id": 123457,
            "model": "HRRR",
            "location_name": "KMDW",
            "latitude": 41.786,
            "longitude": -87.752,
            "run_time": "2025-06-15 18:00:00",
            "valid_time": "2025-06-15 20:00:00",
            "forecast_hour": 2,
            "temperature_k": 300.37,
            "temperature_f": 81.0,
            "temperature_c": 27.2,
            "relative_humidity": 53.0,
            "dew_point_f": 62.1,
            "dew_point_c": 16.7,
            "apparent_temp_f": 83.1,
            "apparent_temp_c": 28.4,
            "wind_speed_mph": 12.1,
            "wind_speed_kmh": 19.5,
            "wind_direction_deg": 215,
            "wind_gusts_mph": null,
            "wind_gusts_kmh": null,
            "cloud_cover": 35.0,
            "precipitation_mm": 0.0,
            "precipitation_in": 0.0,
            "weather_code": 2,
            "pressure_msl_hpa": 1014.0,
            "surface_pressure_hpa": 1009.6,
            "inserted_at": "2025-06-15 18:42:13"
        }
    ]

    Tip: The run_time field represents the model initialization cycle (e.g., 18:00:00 for an 18z run). Combined with inserted_at, you can determine both which cycle a forecast belongs to and how recently it was ingested — useful for staleness decay or freshness-weighted blending across models. Both fields are in UTC.

    Client Examples

    cURL — Forecast Range

    curl -G "https://wethr.net/api/v2/forecasts.php" \
      -H "Authorization: Bearer <YOUR_API_KEY>" \
      --data-urlencode "location_name=KMDW" \
      --data-urlencode "start_valid_time=2025-06-15T18:00:00Z" \
      --data-urlencode "end_valid_time=2025-06-15T20:00:00Z" \
      --data-urlencode "model=HRRR"

    cURL — Latest Run (All Forecast Hours)

    curl -G "https://wethr.net/api/v2/forecasts.php" \
      -H "Authorization: Bearer <YOUR_API_KEY>" \
      --data-urlencode "location_name=KAUS" \
      --data-urlencode "model=HRRR" \
      --data-urlencode "run=latest"

    cURL — Daily High/Low Summary (All Models)

    curl -G "https://wethr.net/api/v2/forecasts.php" \
      -H "Authorization: Bearer <YOUR_API_KEY>" \
      --data-urlencode "location_name=KMDW" \
      --data-urlencode "mode=daily"

    Python — Latest Run

    import requests
    
    response = requests.get(
        'https://wethr.net/api/v2/forecasts.php',
        params={'location_name': 'KAUS', 'model': 'HRRR', 'run': 'latest'},
        headers={'Authorization': f'Bearer {api_key}'}
    )
    forecasts = response.json()
    resolved_run = response.headers.get('X-Run-Time')
    print(f"Latest HRRR run: {resolved_run} ({len(forecasts)} forecast hours)")

    Precipitation API BETA

    GET /api/v2/precipitation.php
    ⚠️ Beta Notice: This API is currently in beta testing. While functional, you may encounter bugs or unexpected behavior. Data accuracy is not guaranteed during this phase. Please report any issues to help us improve the service.

    Returns current month precipitation totals for a station, combining official CLI (Climate Report) data with live ASOS observations. All date/time calculations use Local Standard Time (no DST adjustment) year-round, matching NWS climate reporting standards.

    Endpoint & Parameters

    ParameterRequiredDescription
    station_codeYesStation identifier (see supported stations below)

    Note: This endpoint always returns data for the current month. Historical month data is not available via this API.

    Supported Stations

    KNYC, KLAX, KMDW, KSFO, KAUS, KMIA, KDFW, KHOU, KDEN, KBKF, KSEA, KPHL, KLAS, KPHX, KSAT, KDCA, KCLT, KBOS, KBNA, KATL, KJAX, KOKC, KDTW, KMSP, KMSY

    Example Request

    GET /api/v2/precipitation.php?station_code=KAUS

    Example Response

    {
        "station_code": "KAUS",
        "station_name": "Austin",
        "timezone": "America/Chicago",
        "timezone_offset_hours": -6,
        "today_local_date": "2025-06-15",
        "month": 6,
        "year": 2025,
        "official_mtd": 1.45,
        "today_precip": 0.127,
        "total_mtd": 1.58,
        "has_trace": false,
        "last_trace_time": null,
        "cli_date": "2025-06-14",
        "cli_issued_at": "2025-06-15 05:15:00",
        "today_hourly_max": {
            "2025-06-15 08": 0.05,
            "2025-06-15 09": 0.077
        },
        "timestamp": "2025-06-15T21:30:00Z",
        "units": "inches"
    }

    Response Fields

    FieldTypeDescription
    station_codestringStation identifier
    station_namestringHuman-readable station name
    timezonestringPHP timezone identifier
    timezone_offset_hoursintegerFixed UTC offset for Local Standard Time (e.g., -6 for CST)
    today_local_datestringCurrent date in station's Local Standard Time (YYYY-MM-DD)
    monthintegerCurrent month (1-12)
    yearintegerCurrent year
    official_mtddecimalOfficial month-to-date total from CLI reports (through yesterday)
    today_precipdecimalToday's precipitation calculated from live ASOS observations
    total_mtddecimalCombined total: official_mtd + today_precip
    has_tracebooleanWhether trace precipitation (P0000) was reported today
    last_trace_timestring|nullTime of last trace report (Local Standard Time)
    cli_datestring|nullDate of the most recent CLI report used
    cli_issued_atstring|nullTimestamp when the CLI report was issued
    today_hourly_maxobjectHourly precipitation breakdown for today (hour → max precip)
    timestampstringAPI response timestamp (UTC, ISO 8601)
    unitsstringAlways inches

    Understanding the Data

    • Official (CLI): The official_mtd value comes from the NWS Daily Climate Report (CLI), typically issued around 5 AM local time. This is the "banked" official total through the previous day.
    • Live (Today): The today_precip value is calculated in real-time from 5-minute ASOS observations since midnight Local Standard Time.
    • Trace: When has_trace is true, precipitation was detected but was too small to measure (<0.01"). Trace amounts are not included in the totals.
    • Hourly Max: The today_hourly_max object shows the maximum cumulative reading for each hour, useful for detailed analysis.

    cURL Example

    curl -G "https://wethr.net/api/v2/precipitation.php" \
      -H "Authorization: Bearer <YOUR_API_KEY>" \
      --data-urlencode "station_code=KAUS"

    Python Example

    import requests
    
    response = requests.get(
        'https://wethr.net/api/v2/precipitation.php',
        params={'station_code': 'KAUS'},
        headers={'Authorization': f'Bearer {api_key}'}
    )
    data = response.json()
    
    print(f"Month-to-Date: {data['total_mtd']}\"")
    print(f"  Official (CLI): {data['official_mtd']}\"")
    print(f"  Today (Live):   {data['today_precip']}\"")
    if data['has_trace']:
        print(f"  Trace detected at {data['last_trace_time']}")

    NWS Forecasts API BETA

    GET /api/v2/nws_forecasts.php
    ⚠️ Beta Notice: This API is currently in beta testing. While functional, you may encounter bugs or unexpected behavior. Please report any issues to help us improve the service.

    Returns NWS hourly temperature forecasts for a station. Forecasts are sourced from the National Weather Service API and stored with version tracking as they update throughout the day.

    ⚠️ Critical: Local Standard Time Convention
    All dates and hours in this API use Local Standard Time year-round (no Daylight Saving Time adjustment). This means:
    • During DST months, the "day" boundaries are shifted by 1 hour compared to civil time
    • Hour 0 always represents midnight Standard Time, not midnight local civil time
    • This convention matches NWS climate reporting and Kalshi temperature market resolution

    If you are using a different resolution source that uses civil time (with DST), these forecasts may not align correctly with your day boundaries.

    Endpoint & Parameters

    ParameterRequiredDescription
    station_codeYesStation identifier
    date No Forecast date in YYYY-MM-DD format. Defaults to today. Can request up to 2 days in the future or 365 days (1 year) in the past.
    mode No latest (default) — Returns only the most recent forecast version for the requested date
    history — Returns all forecast versions for the requested date (useful to see how the forecast evolved)

    Example Requests

    # Today's latest forecast
    GET /api/v2/nws_forecasts.php?station_code=KAUS
    
    # Tomorrow's latest forecast
    GET /api/v2/nws_forecasts.php?station_code=KAUS&date=2025-06-16
    
    # Today's forecast history (all versions)
    GET /api/v2/nws_forecasts.php?station_code=KAUS&date=2025-06-15&mode=history

    Example Response (mode=latest)

    {
        "station_code": "KAUS",
        "station_name": "Austin",
        "timezone": "America/Chicago",
        "timezone_offset_hours": -6,
        "forecast_date": "2025-06-15",
        "version": 12,
        "hourly_temps": [null, null, null, null, null, null, null, null, null, null, null, null, null, null, 94, 95, 95, 94, 92, 89, 85, 82, 79, 77, 75],
        "high": 95,
        "low": 75,
        "timestamp": "2025-06-15T21:30:00Z",
        "units": "fahrenheit",
        "time_convention": "local_standard_time"
    }

    Example Response (mode=history)

    {
        "station_code": "KAUS",
        "station_name": "Austin",
        "timezone": "America/Chicago",
        "timezone_offset_hours": -6,
        "forecast_date": "2025-06-15",
        "total_versions": 3,
        "forecasts": [
            {
                "version": 1,
                "hourly_temps": [null, null, null, null, null, null, 71, 74, 78, 82, ...],
                "high": 94,
                "low": 71
            },
            {
                "version": 2,
                "hourly_temps": [null, null, null, null, null, null, null, null, null, null, null, null, 91, 93, ...],
                "high": 95,
                "low": 77
            }
        ],
        "timestamp": "2025-06-15T21:30:00Z",
        "units": "fahrenheit",
        "time_convention": "local_standard_time"
    }

    Response Fields

    FieldTypeDescription
    station_codestringStation identifier
    station_namestringHuman-readable station name
    timezonestringPHP timezone identifier
    timezone_offset_hoursintegerFixed UTC offset for Local Standard Time
    forecast_datestringForecast date (YYYY-MM-DD) in Local Standard Time
    versionintegerForecast version number (latest mode only)
    hourly_tempsarrayArray of 25 temperatures (indices 0-24, where 24 = midnight next day). Values are null for hours that had already passed when the forecast was issued.
    highinteger|nullForecasted high temperature, calculated as the maximum of all non-null values in hourly_temps
    lowinteger|nullForecasted low temperature, calculated as the minimum of all non-null values in hourly_temps
    total_versionsintegerNumber of forecast versions available (history mode only)
    forecastsarrayArray of all forecast versions (history mode only)
    unitsstringAlways fahrenheit
    time_conventionstringAlways local_standard_time

    Understanding the Time Convention

    The hourly_temps array contains 25 values representing hours 0 through 24:

    • Index 0: Midnight (00:00) Local Standard Time
    • Index 12: Noon (12:00) Local Standard Time
    • Index 24: Midnight (00:00) next day — included for continuity

    Null Values for Past Hours

    The NWS API only provides forecasts for future hours. Hours that have already occurred when the forecast was fetched will have null values. For example, if a forecast is fetched at 2:00 PM, indices 0-13 will typically be null while indices 14-24 will contain forecasted temperatures.

    When using mode=history, earlier forecast versions will have more non-null values since they were fetched when more hours were still in the future.

    Example during Daylight Saving Time

    In Austin (CST/CDT), during summer when CDT is in effect:

    • Index 0 represents midnight Central Standard Time (which is 1:00 AM CDT civil time)
    • The "day" in this API runs from 1:00 AM to 1:00 AM civil time during DST months

    cURL Example

    curl -G "https://wethr.net/api/v2/nws_forecasts.php" \
      -H "Authorization: Bearer <YOUR_API_KEY>" \
      --data-urlencode "station_code=KAUS" \
      --data-urlencode "date=2025-06-15"

    Python Example

    import requests
    
    response = requests.get(
        'https://wethr.net/api/v2/nws_forecasts.php',
        params={'station_code': 'KAUS', 'mode': 'latest'},
        headers={'Authorization': f'Bearer {api_key}'}
    )
    data = response.json()
    
    print(f"Forecast for {data['forecast_date']} (version {data['version']})")
    print(f"High: {data['high']}°F / Low: {data['low']}°F")
    print(f"Hourly: {data['hourly_temps']}")

    Push API BETA

    ⚠️ Beta Product: The Push API is currently in beta. While we strive to maintain high availability and data quality, this service may undergo changes without notice. Additionally, weather data is sourced from upstream providers (NOAA, NWS, Synoptic) and station sensors that may occasionally experience outages or report erroneous values. We strongly recommend implementing your own data validation logic to detect and handle anomalies such as unrealistic temperature spikes, sensor malfunctions, or missing data. Wethr.net is not liable for trading decisions or other actions based on data received through this API.

    Overview

    The Push API delivers real-time weather data as events occur. Instead of polling the REST API, clients maintain a persistent connection and receive updates instantly — including new observations, DSM/CLI releases, and temperature extreme alerts.

    Key Benefits:
    • Real-time updates with sub-second latency
    • Efficient — no polling overhead
    • Automatic reconnection with event replay
    • Temperature alerts for new highs/lows as they occur

    Base URL: https://wethr.net:3443/api/v2/stream


    Authentication & Rate Limits

    The Push API uses API key authentication. Include your key as a query parameter:

    GET https://wethr.net:3443/api/v2/stream?stations=KAUS,KMDW&api_key=YOUR_API_KEY
    TierMax StationsConnections per User
    Professional51
    DeveloperUnlimited1
    EnterpriseUnlimited1

    Requirements:

    • HTTPS only
    • One connection per user account
    • API key must be enabled in your account

    Connecting

    Parameters

    ParameterRequiredDescription
    stationsYesComma-separated station codes (e.g., KAUS,KMDW,KLAX)
    api_keyYesYour API key
    last_event_idNoResume from a specific event ID (for reconnection)

    Connection Lifecycle

    1. Client connects with station list and API key
    2. Server validates credentials and station limits
    3. Server sends connected event with subscribed stations
    4. Server streams events as they occur
    5. Server sends periodic heartbeat events (every 30s)
    6. On disconnect, client can reconnect with last_event_id to replay missed events

    Event Types

    EventDescriptionFrequency
    connectedConnection establishedOnce on connect
    heartbeatKeep-alive signalEvery 30 seconds
    observationNew weather observation (METAR/HF-METAR/SPECI)Every 1-5 minutes per station
    dsmDaily Summary Message releasedVaries by station
    cliClimate Report releasedVaries by station
    new_highNew temperature high detectedAs conditions change
    new_lowNew temperature low detectedAs conditions change
    Note — One Minute Observations (OMO): Raw OMO data is not currently available as a Push event type. We anticipate adding OMO events in the coming months. However, the wethr_high and wethr_low values included in new_high and new_low events already incorporate OMO data — if a one minute observation results in a new high or low, these events fire in real time. The wethr_high and wethr_low fields within observation events also reflect the current OMO-informed high and low at the time the observation is reported.

    Payload Reference

    observation

    Sent when a new METAR, HF-METAR, or SPECI observation is received. The example below shows all fields that may be present; some fields are only included conditionally (see the field reference for details).

    {
        "station_code": "KAUS",
        "event": "observation",
        "timezone": "America/Chicago",
        "local_date": "2026-02-08",
        "product": "HF-METAR",
        "observation_time_utc": "2026-02-08T18:55:00Z",
        "temperature_celsius": 24.0,
        "temperature_fahrenheit": 75.2,
        "temperature_precision": "decimal",
        "dew_point_celsius": 12.0,
        "dew_point_fahrenheit": 53.6,
        "lowest_probable_f": 75,
        "highest_probable_f": 75,
        "lowest_probable_c": 24,
        "highest_probable_c": 24,
        "wind_direction": 200,
        "wind_speed_mph": 14,
        "wind_speed_kmh": 22,
        "wind_gust_mph": null,
        "wind_gust_kmh": null,
        "visibility_miles": 10.0,
        "altimeter_hpa": 1019.8,
        "sea_level_pressure_mb": 1019.6,
        "precipitation_inches": 0.0,
        "wethr_high": {
            "nws": {"value_f": 75, "value_c": 24, "time_utc": "2026-02-08T18:44:00Z"},
            "wu": {"value_f": 73, "value_c": 23, "time_utc": "2026-02-08T17:53:00Z"}
        },
        "wethr_low": {
            "nws": {"value_f": 42, "value_c": 6, "time_utc": "2026-02-08T12:53:00Z"},
            "wu": {"value_f": 42, "value_c": 6, "time_utc": "2026-02-08T12:53:00Z"}
        },
        "high_valid_through_utc": "2026-02-08T18:00:00Z",
        "low_valid_through_utc": "2026-02-08T12:00:00Z",
        "anomaly": false,
        "suspect_temperature": null,
        "data_quality": {
            "nws": {"ok": true, "warnings": []},
            "wu": {"ok": true, "warnings": []}
        },
        "cloud_layers": {
            "count": 2,
            "layers": [
                {"height_ft_agl": 3500, "coverage": "SCT"},
                {"height_ft_agl": 12000, "coverage": "BKN"}
            ]
        },
        "six_hour_high_celsius": 24.4,
        "six_hour_high_f": 76,
        "six_hour_low_celsius": 18.3,
        "six_hour_low_f": 65,
        "id": "49500",
        "timestamp": "2026-02-08T18:56:15Z"
    }
    Field Reference — observation
    FieldTypeAlways PresentDescription
    station_codestringYesICAO station identifier (e.g., KAUS)
    eventstringYesAlways "observation" for this event type
    timezonestringYesIANA timezone identifier for the station (e.g., America/Chicago)
    local_datestringYesCurrent local date at the station in YYYY-MM-DD format
    productstringYesSource product: METAR, HF-METAR, or SPECI
    observation_time_utcstringYesUTC timestamp of the observation as reported by the station (ISO 8601)
    temperature_celsiusnumber | nullYesObserved temperature in Celsius (2 decimal places when source is precise, otherwise rounded). null if rejected — see suspect_temperature.
    temperature_fahrenheitnumber | nullYesObserved temperature in Fahrenheit (1 decimal place). null if rejected.
    temperature_precisionstringYesSource precision: "decimal" if the observation reports a 1-decimal value (e.g., HF-METAR with T-group), or "integer" if reported only as a whole degree. Determines whether lowest_probable/highest_probable are a single value or a range.
    dew_point_celsiusnumber | nullYesDew point in Celsius. null if dew point depression exceeds 12°C (treated as suspect).
    dew_point_fahrenheitnumber | nullYesDew point in Fahrenheit
    lowest_probable_finteger | nullYesLower bound of the rounding-uncertainty range for the current observation's temperature in °F. When temperature_precision is "integer", this is the lowest value that could have rounded to the reported temperature. When precision is "decimal", this equals highest_probable_f. Not the day's low — see wethr_low for that.
    highest_probable_finteger | nullYesUpper bound of the rounding-uncertainty range for the current observation in °F. See lowest_probable_f.
    lowest_probable_cinteger | nullYesLower bound of the rounding-uncertainty range in °C
    highest_probable_cinteger | nullYesUpper bound of the rounding-uncertainty range in °C
    wind_directioninteger | nullYesWind direction in degrees (0–360). null when calm or variable.
    wind_speed_mphinteger | nullYesSustained wind speed in miles per hour (converted from knots)
    wind_speed_kmhinteger | nullYesSustained wind speed in kilometers per hour
    wind_gust_mphinteger | nullYesWind gust speed in mph. null when no gust was reported or when the gust value failed validation (e.g., gust less than sustained speed).
    wind_gust_kmhinteger | nullYesWind gust speed in km/h
    visibility_milesnumber | nullYesVisibility in statute miles. Values reported as "10+" in METAR are normalized to 10.0.
    altimeter_hpanumber | nullYesAltimeter setting in hectopascals (mb-equivalent)
    sea_level_pressure_mbnumber | nullYesSea level pressure in millibars (only present in hourly METARs that include the SLP group)
    precipitation_inchesnumber | nullYesPrecipitation amount reported in this observation, in inches. Typically the hourly precipitation group from a METAR.
    wethr_highobjectYesDay's high temperature computed two ways. See Wethr High/Low below.
    wethr_lowobjectYesDay's low temperature computed two ways. See Wethr High/Low below.
    high_valid_through_utcstring | nullYesUTC timestamp through which the wethr_high is considered locked in. null means the high is still based on preliminary observations and may change. See Valid Through below.
    low_valid_through_utcstring | nullYesUTC timestamp through which the wethr_low is considered locked in
    anomalybooleanYesSet to true when an upstream DSM/CLI high looked suspicious compared to surrounding METAR observations. See Data Quality Flags.
    suspect_temperatureobject | nullYesnull in the normal case. When this observation's temperature failed plausibility checks, this object holds the rejected value and reason, and temperature_celsius/temperature_fahrenheit are null. See Data Quality Flags.
    data_qualityobjectYesPer-resolution-mode report of missing temperature readings (-999) in the day's window, keyed nws and wu. Each holds ok and a warnings array. See Data Quality Flags.
    cloud_layersobjectYesCloud cover layers reported in the observation. See Cloud Layers below for structure.
    six_hour_high_celsiusnumberConditional6-hour maximum temperature in Celsius. Present only when this observation includes the 6-hour group (typically the :53 hourly METAR every 6 hours: 00z, 06z, 12z, 18z).
    six_hour_high_fintegerConditional6-hour maximum in Fahrenheit (rounded). Present when six_hour_high_celsius is.
    six_hour_low_celsiusnumberConditional6-hour minimum temperature in Celsius. Same presence rules as six_hour_high_celsius.
    six_hour_low_fintegerConditional6-hour minimum in Fahrenheit
    twentyfour_hour_high_celsiusnumberConditional24-hour maximum temperature in Celsius. Present only on the 00z and 12z synoptic observations that report the 24-hour group.
    twentyfour_hour_high_fintegerConditional24-hour maximum in Fahrenheit
    twentyfour_hour_low_celsiusnumberConditional24-hour minimum temperature in Celsius
    twentyfour_hour_low_fintegerConditional24-hour minimum in Fahrenheit
    idstringYesUnique monotonic event ID. Use this with last_event_id to replay missed events after reconnection.
    timestampstringYesUTC timestamp when Wethr.net published this event (ISO 8601). Note this is the publish time, not the observation time — see observation_time_utc.
    Wethr High / Low Objects

    Both wethr_high and wethr_low contain two parallel calculations of the day's temperature extreme. You should choose the one that matches your settlement methodology.

    LogicInputsTime Window
    nwsAll observations (METAR, SPECI, HF-METAR, OMO) plus official DSM/CLI reports once releasedStandard Time window (no DST adjustment), matching NWS climate reporting
    wuMETAR observations onlyLocal clock time window (DST-aware)

    Each logic block contains value_f (integer °F), value_c (integer °C), and time_utc (UTC timestamp when the extreme occurred, or null if no extreme has been recorded yet today).

    Valid Through (HVT / LVT)

    high_valid_through_utc and low_valid_through_utc indicate the time through which the high/low values are considered "locked in" based on official reports (DSM/CLI) and confirmed 6-hour extremes. A null value means the extreme is still based on preliminary observations and may change. Once the official daily summary is released, these fields become non-null and the wethr_high/wethr_low values for the trading day are final.

    Cloud Layers Object

    The cloud_layers object summarizes the cloud cover groups reported in the METAR. It always has the following shape:

    FieldTypeDescription
    countinteger | nullNumber of cloud layers reported (0–3). null if the source did not report a cloud layer count.
    layersarrayOrdered array of layer objects, lowest base first. May be empty (clear skies or no data).

    Each layer object contains:

    FieldTypeDescription
    height_ft_aglinteger | nullCloud base height in feet above ground level
    coveragestring | nullCoverage code: CLR (clear), FEW (few, 1/8–2/8), SCT (scattered, 3/8–4/8), BKN (broken, 5/8–7/8), OVC (overcast, 8/8). May also be VV (vertical visibility) in obscured conditions.

    A layer is included whenever either height_ft_agl or coverage is non-null, so consumers should not assume both fields are always populated on every layer.

    Data Quality Flags: anomaly, suspect_temperature, and data_quality help identify potentially erroneous or incomplete data. See Data Quality Flags below for details.

    dsm

    Sent when the Daily Summary Message is released by the National Weather Service for this station. Timing varies by station but typically occurs once per day after the climatological day ends.

    {
        "station_code": "KAUS",
        "event": "dsm",
        "timezone": "America/Chicago",
        "for_date": "2026-02-07",
        "valid_through_utc": "2026-02-08T06:00:00Z",
        "high_f": 84,
        "high_c": 29,
        "high_time_utc": "2026-02-07T21:53:00Z",
        "low_f": 42,
        "low_c": 6,
        "low_time_utc": "2026-02-07T12:53:00Z",
        "anomaly": false,
        "id": "49123",
        "timestamp": "2026-02-08T06:15:00Z"
    }
    Field Reference — dsm
    FieldTypeDescription
    station_codestringICAO station identifier
    eventstringAlways "dsm"
    timezonestringIANA timezone identifier for the station
    for_datestringThe climatological date this summary covers, in YYYY-MM-DD format (local date)
    valid_through_utcstringUTC timestamp through which these values are considered final/locked in
    high_finteger | nullDaily high temperature in Fahrenheit as reported in the DSM
    high_cnumber | nullDaily high temperature in Celsius (2 decimal places)
    high_time_utcstring | nullUTC timestamp at which the high occurred, when included in the DSM
    low_finteger | nullDaily low temperature in Fahrenheit
    low_cnumber | nullDaily low temperature in Celsius
    low_time_utcstring | nullUTC timestamp at which the low occurred, when included in the DSM
    anomalybooleantrue when the DSM high looks suspiciously elevated vs. nearby METAR observations. See Data Quality Flags.
    idstringUnique monotonic event ID
    timestampstringUTC publish time

    cli

    Sent when the Climate Report (CLI product) is released for this station. Like the DSM, timing varies by station.

    {
        "station_code": "KAUS",
        "event": "cli",
        "timezone": "America/Chicago",
        "for_date": "2026-02-07",
        "valid_through_utc": "2026-02-08T06:00:00Z",
        "high_f": 84,
        "high_c": 29,
        "high_time_utc": "2026-02-07T21:53:00Z",
        "low_f": 42,
        "low_c": 6,
        "low_time_utc": "2026-02-07T12:53:00Z",
        "anomaly": false,
        "id": "49456",
        "timestamp": "2026-02-08T14:30:00Z"
    }
    Field Reference — cli
    FieldTypeDescription
    station_codestringICAO station identifier
    eventstringAlways "cli"
    timezonestringIANA timezone identifier for the station
    for_datestringThe climatological date this report covers (YYYY-MM-DD local)
    valid_through_utcstringUTC timestamp through which these values are considered final/locked in
    high_finteger | nullDaily high temperature in Fahrenheit as reported in the CLI
    high_cnumber | nullDaily high temperature in Celsius (2 decimal places)
    high_time_utcstring | nullUTC timestamp at which the high occurred, when included in the CLI
    low_finteger | nullDaily low temperature in Fahrenheit
    low_cnumber | nullDaily low temperature in Celsius
    low_time_utcstring | nullUTC timestamp at which the low occurred, when included in the CLI
    anomalybooleantrue when the CLI high looks suspiciously elevated vs. nearby METAR observations
    idstringUnique monotonic event ID
    timestampstringUTC publish time

    new_high / new_low

    Sent immediately when a new temperature extreme is detected for the trading day. These are leaner than observation events — they only carry the changed extreme and the value it replaced.

    {
        "station_code": "KAUS",
        "event": "new_high",
        "logic": "nws",
        "value_f": 75,
        "value_c": 24,
        "prev_value_f": 73,
        "prev_value_c": 23,
        "observation_time_utc": "2026-02-08T18:44:00Z",
        "id": "49478",
        "timestamp": "2026-02-08T18:46:29Z"
    }

    The logic field indicates which calculation triggered the alert (nws or wu). You may receive separate alerts for each logic if both detect a new extreme — for example, a single observation may produce one new_high with logic: "nws" and a second new_high with logic: "wu".

    Field Reference — new_high / new_low
    FieldTypeDescription
    station_codestringICAO station identifier
    eventstringEither "new_high" or "new_low"
    logicstringWhich calculation methodology triggered this alert: "nws" or "wu". See Wethr High/Low.
    value_fintegerThe new extreme temperature in Fahrenheit
    value_cintegerThe new extreme temperature in Celsius
    prev_value_finteger | nullThe prior extreme this replaced, in Fahrenheit. null if this is the first extreme recorded today.
    prev_value_cinteger | nullThe prior extreme this replaced, in Celsius
    observation_time_utcstringUTC timestamp of the observation that produced the new extreme
    idstringUnique monotonic event ID
    timestampstringUTC publish time

    Data Quality Flags

    The Push API includes flags to help you identify potentially erroneous data. We strongly recommend checking these fields before using observation data for trading or other high-stakes decisions.

    FieldEvent(s)TypeDescription
    anomaly observation, dsm, cli boolean DSM/CLI high temperature is suspiciously elevated vs. nearby METAR observations (possible erroneous official report)
    suspect_temperature observation object | null METAR temperature failed plausibility checks. When present, temperature_celsius / temperature_fahrenheit are null. The rejected values and reason are preserved in this object. The implausible reading is excluded from all calculations — it will not affect wethr_high, wethr_low, or trigger new_high / new_low alerts.
    data_quality observation object Per-mode (nws/wu) report of missing temperature readings (the -999 sentinel) across the day's window. Each mode has ok and a warnings array. Unlike suspect_temperature (one rejected reading), this signals incomplete coverage that can leave the reported high/low understated. See data_quality (Missing Temperature) below.

    suspect_temperature Object

    FieldTypeDescription
    raw_celsiusnumberThe rejected temperature in Celsius
    raw_fahrenheitnumberThe rejected temperature in Fahrenheit
    reasonstringMachine-readable reason code (currently: out_of_range)
    messagestringHuman-readable description

    Example: Suspect Temperature Event

    {
        "station_code": "KLAX",
        "event": "observation",
        "product": "METAR",
        "temperature_celsius": null,
        "temperature_fahrenheit": null,
        "suspect_temperature": {
            "raw_celsius": -61.0,
            "raw_fahrenheit": -77.8,
            "reason": "out_of_range",
            "message": "Temperature outside plausible range (-60°C to 60°C)"
        },
        "wethr_high": {
            "nws": {"value_f": 68, "value_c": 20, "time_utc": "2026-02-08T21:53:00Z"},
            "wu": {"value_f": 68, "value_c": 20, "time_utc": "2026-02-08T21:53:00Z"}
        },
        "anomaly": false,
        "id": "49501",
        "timestamp": "2026-02-08T22:56:15Z"
    }

    In this example, KLAX reported −61°C which is clearly erroneous. The temperature was rejected and preserved in suspect_temperature for transparency. The wethr_high and wethr_low values are unaffected, and no new_low alert was triggered.

    data_quality (Missing Temperature) NEW

    The observation event includes a data_quality object that reports gaps in the day's temperature observations — readings the station logged as missing (the -999 sentinel). Unlike suspect_temperature (a single rejected reading), this looks across the whole trading-day window and warns when temperature coverage was incomplete, which can leave the reported wethr_high/wethr_low understated.

    It is keyed per resolution mode (nws and wu) because each mode scans its own window. Each mode holds an envelope:

    FieldTypeDescription
    okbooleantrue when no problems were found (the warnings array is empty)
    warningsarrayZero or more warning objects (below). Currently the only warning is temperature_data_missing; the array allows for additional checks in future without changing the shape.

    Each warning object:

    FieldTypeDescription
    codestringStable identifier: "temperature_data_missing"
    severitystring"warning" or "critical" (see escalation rules below)
    messagestringHuman-readable summary. Treat as display text only — read the structured fields below for logic.
    missing_countintegerTotal number of missing (-999) temperature readings in the window
    longest_runintegerLength of the longest run of consecutive missing readings
    longest_run_minutesintegerDuration of that longest run, in minutes
    first_missing_utcstringUTC time of the first missing reading (YYYY-MM-DD HH:MM:SS)
    last_missing_utcstringUTC time of the last missing reading
    activebooleantrue when the most recent reading is missing — the station appears to still be down
    active_runintegerLength of the current trailing run of missing readings (0 when not active)
    near_extremebooleantrue when a missing reading falls near the reported high or low (i.e. near_high or near_low)
    near_highbooleantrue when a gap falls within ~60 minutes of the reported high's time
    near_lowbooleantrue when a gap falls within ~60 minutes of the reported low's time

    Severity. Any missing reading produces at least a warning. It escalates to critical when the station is currently down (active) or when a gap sits near the reported high or low (near_high / near_low) — the case where the reported extreme is most likely to be wrong, since the true high/low could have occurred during the gap.

    Example: gap near the day's high
    "data_quality": {
        "nws": {
            "ok": false,
            "warnings": [
                {
                    "code": "temperature_data_missing",
                    "severity": "critical",
                    "message": "2 temperature readings missing (-999) in this window; longest gap 2 consecutive (~10 min) — a gap falls within ~60 min of the day's high, so wethr_high/wethr_low may be unreliable.",
                    "missing_count": 2,
                    "longest_run": 2,
                    "longest_run_minutes": 10,
                    "first_missing_utc": "2026-02-08 20:43:00",
                    "last_missing_utc": "2026-02-08 20:48:00",
                    "active": false,
                    "active_run": 0,
                    "near_extreme": true,
                    "near_high": true,
                    "near_low": false
                }
            ]
        },
        "wu": {"ok": true, "warnings": []}
    }

    Here the NWS window had a two-reading gap near the time of the day's high, so its high may be understated and the warning is critical; the WU window was complete. In the REST Observations API the same object appears, keyed by the single logic requested rather than both modes.


    Client Examples

    cURL — Test Connection

    curl -N "https://wethr.net:3443/api/v2/stream?stations=KAUS,KMDW&api_key=YOUR_API_KEY"

    JavaScript (Browser)

    const apiKey = 'YOUR_API_KEY';
    const stations = ['KAUS', 'KMDW', 'KLAX'];
    
    const url = `https://wethr.net:3443/api/v2/stream?stations=${stations.join(',')}&api_key=${apiKey}`;
    const eventSource = new EventSource(url);
    
    eventSource.addEventListener('observation', (e) => {
        const data = JSON.parse(e.data);
        
        // Check for suspect temperature
        if (data.suspect_temperature) {
            console.warn(`⚠️ ${data.station_code}: Suspect temp rejected (${data.suspect_temperature.raw_fahrenheit}°F - ${data.suspect_temperature.reason})`);
            return;
        }
        
        console.log(`${data.station_code}: ${data.temperature_fahrenheit}°F`);
        console.log(`  Wethr High (NWS): ${data.wethr_high.nws.value_f}°F`);
    });
    
    eventSource.addEventListener('new_high', (e) => {
        const data = JSON.parse(e.data);
        console.log(`🔥 NEW HIGH at ${data.station_code}: ${data.value_f}°F (was ${data.prev_value_f}°F)`);
    });
    
    eventSource.addEventListener('new_low', (e) => {
        const data = JSON.parse(e.data);
        console.log(`❄️ NEW LOW at ${data.station_code}: ${data.value_f}°F (was ${data.prev_value_f}°F)`);
    });
    
    eventSource.onerror = (e) => {
        console.error('Connection error:', e);
    };

    Python

    import sseclient
    import requests
    import json
    
    api_key = 'YOUR_API_KEY'
    stations = 'KAUS,KMDW,KLAX'
    url = f'https://wethr.net:3443/api/v2/stream?stations={stations}&api_key={api_key}'
    
    response = requests.get(url, stream=True)
    client = sseclient.SSEClient(response)
    
    for event in client.events():
        if event.event == 'observation':
            data = json.loads(event.data)
            
            # Check for suspect temperature
            if data.get('suspect_temperature'):
                st = data['suspect_temperature']
                print(f"⚠️ {data['station_code']}: Suspect temp rejected ({st['raw_fahrenheit']}°F - {st['reason']})")
                continue
            
            print(f"{data['station_code']}: {data['temperature_fahrenheit']}°F")
            print(f"  Wethr High (NWS): {data['wethr_high']['nws']['value_f']}°F")
        
        elif event.event == 'new_high':
            data = json.loads(event.data)
            print(f"🔥 NEW HIGH at {data['station_code']}: {data['value_f']}°F")

    Node.js

    const EventSource = require('eventsource');
    
    const apiKey = 'YOUR_API_KEY';
    const stations = 'KAUS,KMDW,KLAX';
    const url = `https://wethr.net:3443/api/v2/stream?stations=${stations}&api_key=${apiKey}`;
    
    const es = new EventSource(url);
    
    es.addEventListener('observation', (e) => {
        const data = JSON.parse(e.data);
        console.log(`${data.station_code}: ${data.temperature_fahrenheit}°F`);
    });
    
    es.addEventListener('new_high', (e) => {
        const data = JSON.parse(e.data);
        console.log(`NEW HIGH at ${data.station_code}: ${data.value_f}°F`);
    });
    
    es.onerror = (err) => {
        console.error('Error:', err);
    };

    Reconnection with Event Replay

    If your connection drops, you can resume from where you left off using the last_event_id parameter. Track the id field from each event and pass it on reconnect:

    let lastEventId = null;
    
    function connect() {
        let url = `https://wethr.net:3443/api/v2/stream?stations=KAUS&api_key=${apiKey}`;
        if (lastEventId) {
            url += `&last_event_id=${lastEventId}`;
        }
        
        const es = new EventSource(url);
        
        es.onmessage = (e) => {
            const data = JSON.parse(e.data);
            lastEventId = data.id;  // Track for reconnection
        };
        
        es.onerror = () => {
            es.close();
            setTimeout(connect, 5000);  // Reconnect after 5s
        };
    }
    
    connect();

    Model Accuracy API NEW DEV+

    GET /api/v2/model_accuracy.php
    🔒 Developer & Enterprise Only: This endpoint requires a Developer or Enterprise tier API key. Professional and Testing tier keys will receive a 403 Access Denied response.

    Returns per-model forecast accuracy metrics for a station, including MAE (Mean Absolute Error), bias, and RMSE across configurable time windows. Data is computed daily from verified high or low temperature observations compared against raw model output. You can query either the daily high (default) or daily low via the extreme parameter.

    All 66 stations (US and international) are supported. Data is updated once daily and cached server-side for performance.

    📐 About the bias field — read this before integrating:

    The bias field returned by this endpoint is computed as actual − forecast. Mathematically, this value represents the correction factor — the number you would add to the model's forecast to match the average actual on these days.

    • Positive bias = the model under-predicted (forecast was lower than what actually occurred). The model "ran cold."
    • Negative bias = the model over-predicted (forecast was higher than what actually occurred). The model "ran warm."

    If you prefer the conventional bias measure used in most forecast verification literature — where positive means a warm-running model — simply negate the value: conventional_bias = -bias. Both representations describe the same underlying error in opposite sign conventions. The field name and value are preserved as-is for backward compatibility with existing integrations.

    Endpoint & Parameters

    ParameterRequiredDescription
    station_code Yes ICAO station identifier (e.g., KORD, EGLC)
    extreme No Which daily extreme to query. Values: high (default), low. Determines whether the response describes daily high or daily low temperature accuracy.
    window No Time window for accuracy metrics. Values: 7d (default), 14d, 30d, 90d
    model No Comma-separated model filter (case-sensitive). Example: GFS,HRRR,ECMWF-IFS. Omit for all models.
    run_time No Comma-separated run time filter. Example: 00z,12z. Omit for all run times.
    lead_time No Comma-separated lead-time bucket filter. Lead time is the gap between when a model run was issued and when the actual extreme occurred. Valid bucket labels: 0-3h, 3-6h, 6-12h, 12-18h, 18-24h. Example: 0-3h,3-6h. Omit for all buckets.
    include No Comma-separated list of optional data sections to include: trends, daily_detail. See Optional Includes.

    Available Models: ARPEGE, ECMWF-IFS, GEFS, GEM-GDPS, GEM-HRDPS, GFS, GFS-MOS, HRRR, ICON, JMA, LAV-MOS, NAM, NAM-MOS, NAM4KM, NBM, NBS-MOS, NWS, RAP, UKMO

    Note: Not all models have data for all run times or all stations. The response will only include models and runs that have available data within the requested window.

    Example Requests

    # Default: high temperature, all models, 7-day window, core metrics only
    GET /api/v2/model_accuracy.php?station_code=KORD
    
    # Low temperature accuracy
    GET /api/v2/model_accuracy.php?station_code=KORD&extreme=low
    
    # Specific models and window
    GET /api/v2/model_accuracy.php?station_code=KMIA&window=30d&model=GFS,HRRR,ECMWF-IFS
    
    # Filter by run time
    GET /api/v2/model_accuracy.php?station_code=KDTW&window=14d&model=GFS&run_time=12z,18z
    
    # Filter by lead time (only forecasts issued within 6 hours of the actual extreme)
    GET /api/v2/model_accuracy.php?station_code=KAUS&window=30d&lead_time=0-3h,3-6h
    
    # Low temperature with daily detail
    GET /api/v2/model_accuracy.php?station_code=KAUS&extreme=low&window=7d&include=daily_detail
    
    # Include trend data and daily detail
    GET /api/v2/model_accuracy.php?station_code=KAUS&window=7d&include=trends,daily_detail

    Example Response (Default — No Includes)

    {
        "station": "KDTW",
        "city": "Detroit, MI",
        "extreme_type": "high",
        "computed_at": "2026-03-15T02:15:23.286094",
        "window": "7d",
        "models": ["HRRR", "GFS", "ECMWF-IFS"],
        "days_available": 6,
        "ensemble": {
            "mae": 3.94,
            "bias": 3.94,
            "rmse": 4.77,
            "n": 6
        },
        "per_model": {
            "HRRR": {"mae": 1.79, "bias": 1.61, "rmse": 2.17, "n": 6},
            "GFS": {"mae": 3.32, "bias": 2.88, "rmse": 4.19, "n": 6},
            "ECMWF-IFS": {"mae": 3.83, "bias": 3.17, "rmse": 4.53, "n": 6}
        },
        "per_model_run": {
            "HRRR": {
                "13z": {"mae": 1.77, "bias": 1.17, "rmse": 2.37, "n": 6},
                "16z": {"mae": 1.14, "bias": 0.75, "rmse": 1.32, "n": 4},
                "18z": {"mae": 0.78, "bias": 0.23, "rmse": 0.82, "n": 4}
            },
            "GFS": {
                "06z": {"mae": 3.5, "bias": 3.5, "rmse": 4.41, "n": 6},
                "12z": {"mae": 3.87, "bias": 3.25, "rmse": 5.11, "n": 5},
                "18z": {"mae": 2.85, "bias": 2.85, "rmse": 3.32, "n": 2}
            },
            "ECMWF-IFS": {
                "06z": {"mae": 4.33, "bias": 4.0, "rmse": 5.4, "n": 6},
                "12z": {"mae": 3.71, "bias": 2.57, "rmse": 4.26, "n": 5}
            }
        },
        "per_model_lead": {
            "HRRR": {
                "0-3h":   {"mae": 1.21, "bias": 0.45, "rmse": 1.58, "n": 12},
                "3-6h":   {"mae": 1.46, "bias": 0.83, "rmse": 1.79, "n": 14},
                "6-12h":  {"mae": 1.92, "bias": 1.34, "rmse": 2.31, "n": 22},
                "12-18h": {"mae": 2.41, "bias": 1.97, "rmse": 2.88, "n": 18}
            },
            "GFS": {
                "0-3h":   {"mae": 2.83, "bias": 2.20, "rmse": 3.41, "n": 6},
                "3-6h":   {"mae": 3.05, "bias": 2.55, "rmse": 3.78, "n": 8},
                "6-12h":  {"mae": 3.51, "bias": 3.02, "rmse": 4.22, "n": 14},
                "12-18h": {"mae": 4.17, "bias": 3.81, "rmse": 4.93, "n": 12}
            },
            "ECMWF-IFS": {
                "6-12h":  {"mae": 3.78, "bias": 3.10, "rmse": 4.45, "n": 7},
                "12-18h": {"mae": 4.21, "bias": 3.55, "rmse": 4.89, "n": 9}
            }
        }
    }

    Note: This example shows a model=HRRR,GFS,ECMWF-IFS filtered response. Without the model filter, all 19 tracked models would be returned.

    Response Fields

    Top-Level Fields

    FieldTypeDescription
    stationstringICAO station code
    citystringHuman-readable city name
    extreme_typestringWhich extreme this response describes: high or low
    computed_atstringISO 8601 timestamp of when the accuracy data was last computed
    windowstringTime window requested (7d, 14d, 30d, 90d)
    modelsarrayList of model names included in this response (affected by model filter)
    days_availableintegerNumber of days with valid data in this window

    ensemble

    Aggregate accuracy metrics across all models for the requested window.

    FieldTypeDescription
    maedecimalMean Absolute Error (°F) — average distance between forecast and actual, ignoring direction
    biasdecimalMean signed error, computed as actual − forecast. Mathematically equivalent to a correction factor: adding this value to the model's forecast yields the average actual on these days. Positive = forecast was too low, model "ran cold" (under-predicted). Negative = forecast was too high, model "ran warm" (over-predicted). To get the conventional bias measure (positive = warm-running model), negate this value: conventional_bias = -bias.
    rmsedecimalRoot Mean Square Error — like MAE but penalizes large misses more heavily
    nintegerNumber of valid forecast-observation pairs

    per_model

    Object keyed by model name. Each value contains the same mae, bias, rmse, n fields as the ensemble, but scoped to that specific model. For each day in the window, the model's most recent qualifying run before the actual extreme is used — i.e. the freshest forecast available. Sample sizes here equal the number of days in the window.

    per_model_run

    Object keyed by model name, then by run time label (e.g., 00z, 06z, 12z, 18z). Each run time entry contains mae, bias, rmse, n for that specific model + run time combination.

    Run time labels are in UTC (e.g., 12z = 12:00 UTC model initialization). Available run times vary by model — for example, HRRR may have 13z, 16z, 18z while GFS has 06z, 12z, 18z. Unlike per_model, every qualifying run is counted here, so sample sizes are typically larger than the per-model table.

    per_model_lead

    Object keyed by model name, then by lead-time bucket label. Lead time is the gap between when a model run was issued and when the actual extreme occurred. This breakdown shows how forecast accuracy varies with how far in advance the prediction was made.

    Bucket labels (in order): 0-3h, 3-6h, 6-12h, 12-18h, 18-24h. Generally, shorter lead times produce more accurate forecasts. Useful for evaluating how much weight to assign a given model run based on how recently it was issued.

    Each bucket entry contains mae, bias, rmse, n for that specific model + lead-time combination. Like per_model_run, every qualifying run contributes one data point per bucket — so the same run can appear in this breakdown and the run-time breakdown.


    Optional Includes

    By default, the response contains only the core metrics (per_model, per_model_run, per_model_lead, ensemble) for the requested window. Use the include parameter to add heavier data sections. All optional includes respect the model filter — only data for requested models is returned.

    trends

    Rolling 7-day MAE and bias over time, keyed by model name. Each entry is an array of data points:

    {
        "trends": {
            "HRRR": [
                {"date": "2025-12-25", "mae_7d": 0.39, "bias_7d": -0.39, "n": 2},
                {"date": "2025-12-26", "mae_7d": 0.55, "bias_7d": -0.10, "n": 3},
                ...
            ],
            "GFS": [ ... ]
        }
    }

    Typically contains ~80 data points per model. Useful for charting model performance over time.

    Note on bias_7d: uses the same convention as the bias field (actual − forecast, equivalent to a correction factor). Negate it if you want the conventional bias measure.

    daily_detail

    Last 30 days of actual vs. predicted temperatures for each model, including which run time was used. The actual-temperature field name depends on which extreme you queried: actual_high when extreme=high, actual_low when extreme=low.

    {
        "daily_detail": [
            {
                "date": "2026-03-14",
                "actual_high": 41,
                "models": {
                    "HRRR": 41.5,
                    "GFS": 40.5,
                    "ECMWF-IFS": 39.7
                },
                "run_times": {
                    "HRRR": "18z",
                    "GFS": "18z",
                    "ECMWF-IFS": "12z"
                }
            },
            ...
        ]
    }

    Payload Size Reference

    Approximate response sizes for a typical station (19 models):

    RequestSize
    Default (no includes)~4 KB
    2 models filtered, no includes~400 bytes
    All includes, no model filter~120 KB

    Client Examples

    cURL — Default (7-Day, All Models)

    curl -G "https://wethr.net/api/v2/model_accuracy.php" \
      -H "Authorization: Bearer <YOUR_API_KEY>" \
      --data-urlencode "station_code=KORD"

    cURL — Filtered by Model and Run Time

    curl -G "https://wethr.net/api/v2/model_accuracy.php" \
      -H "Authorization: Bearer <YOUR_API_KEY>" \
      --data-urlencode "station_code=KDTW" \
      --data-urlencode "window=30d" \
      --data-urlencode "model=GFS,HRRR,ECMWF-IFS" \
      --data-urlencode "run_time=12z,18z"

    cURL — Low Temperature Accuracy

    curl -G "https://wethr.net/api/v2/model_accuracy.php" \
      -H "Authorization: Bearer <YOUR_API_KEY>" \
      --data-urlencode "station_code=KAUS" \
      --data-urlencode "extreme=low" \
      --data-urlencode "window=30d"

    cURL — Filter by Lead Time

    curl -G "https://wethr.net/api/v2/model_accuracy.php" \
      -H "Authorization: Bearer <YOUR_API_KEY>" \
      --data-urlencode "station_code=KORD" \
      --data-urlencode "window=30d" \
      --data-urlencode "lead_time=0-3h,3-6h"

    cURL — With Trend Data

    curl -G "https://wethr.net/api/v2/model_accuracy.php" \
      -H "Authorization: Bearer <YOUR_API_KEY>" \
      --data-urlencode "station_code=KAUS" \
      --data-urlencode "window=90d" \
      --data-urlencode "model=HRRR,NBM" \
      --data-urlencode "include=trends"

    Python — Compare Models Across Windows

    import requests
    
    api_key = 'YOUR_API_KEY'
    headers = {'Authorization': f'Bearer {api_key}'}
    
    for window in ['7d', '14d', '30d', '90d']:
        resp = requests.get(
            'https://wethr.net/api/v2/model_accuracy.php',
            params={
                'station_code': 'KORD',
                'window': window,
                'model': 'GFS,HRRR,ECMWF-IFS'
            },
            headers=headers
        )
        data = resp.json()
        print(f"\n--- {window} ({data['days_available']} days) ---")
        for model, stats in data['per_model'].items():
            print(f"  {model}: MAE={stats['mae']:.1f}° Bias={stats['bias']:+.1f}° (n={stats['n']})")

    JavaScript — Fetch with Daily Detail

    const params = new URLSearchParams({
        station_code: 'KMIA',
        window: '30d',
        model: 'HRRR,NBM,GFS',
        include: 'daily_detail'
    });
    
    fetch(`/api/v2/model_accuracy.php?${params}`, {
        headers: { 'Authorization': `Bearer ${apiKey}` }
    })
    .then(res => res.json())
    .then(data => {
        console.log(`Station: ${data.station} (${data.city}) — ${data.extreme_type}`);
        console.log(`Ensemble MAE: ${data.ensemble.mae}°F`);
    
        // Show each model's performance
        for (const [model, stats] of Object.entries(data.per_model)) {
            console.log(`  ${model}: MAE=${stats.mae}° Bias=${stats.bias > 0 ? '+' : ''}${stats.bias}°`);
        }
    
        // The actual-temperature field name depends on extreme_type
        const actualField = data.extreme_type === 'low' ? 'actual_low' : 'actual_high';
    
        // Show daily predictions vs actual
        data.daily_detail.forEach(day => {
            const preds = Object.entries(day.models)
                .map(([m, p]) => `${m}=${p}°`)
                .join(', ');
            console.log(`  ${day.date}: Actual=${day[actualField]}° | ${preds}`);
        });
    });

    Nearby Stations API NEW BETA DEV+

    GET /api/v2/nearby.php
    🔒 Developer & Enterprise Only: This endpoint requires a Developer or Enterprise tier API key. Professional and Testing tier keys will receive a 403 Access Denied response.
    ⚠️ Beta Notice: This API is currently in beta testing. While functional, you may encounter bugs, response shape changes, or unexpected behavior as we iterate. Default parameter values, in particular, may be revised. Please report any issues to help us improve the service.

    Returns weather stations near a target station along with each station's latest observation. Two operating modes:

    • Radius mode (default): given a target station and a radius, returns every station within that radius whose most recent observation is fresher than the max_age_minutes cutoff, sorted by ascending distance.
    • Explicit list mode: given a comma-separated list of station codes in the neighbors parameter, returns the latest observation for each (still filtered by max_age_minutes, still sorted by distance from the target). The radius is ignored in this mode.

    The target station's own observation can optionally be embedded in the response. Bearing from the target to each neighbor is included by default. All catalogued stations are eligible — both ICAO stations and mesonet/CWOP sites.

    ⏱ About the default max_age_minutes=10:

    The default freshness cutoff is deliberately tight. Many METAR stations report only at the top of the hour, and mesonet stations push at variable intervals — so a 10-minute window will often yield count: 0 even at busy locations. This is correct behavior, not a failure. If you want broader coverage, raise the cutoff (e.g., max_age_minutes=30 for typical use; 60 or higher for sparse rural areas). The maximum allowed is 1440 (24 hours), matching the rolling data retention window.

    Endpoint & Parameters

    ParameterRequiredDescription
    station_code Yes Target station identifier. Must exist in the station catalog with valid coordinates. Examples: KORD, EGLC, KMDW.
    radius_km No Search radius in kilometers. Default 10. Maximum 32.2 (≈ 20 mi). Ignored when neighbors is provided.
    radius_mi No Search radius in miles. Alternative to radius_km. Internally converted (1 mi ≈ 1.60934 km). Maximum 20 (equivalent to the 32.2 km cap). Ignored when neighbors is provided.
    neighbors No Comma-separated list of station codes to look up directly (max 25). When present, switches the endpoint into explicit list mode — radius/limit are ignored and only the listed stations are returned. Example: KORD,KDPA,KPWK,KGYY.
    max_age_minutes No Maximum age in minutes for a station's most recent observation to be eligible. Default 10. Range 51440. See the note above.
    limit No Maximum number of neighbors to return (sorted nearest-first). Default 25. Range 1100. Ignored when neighbors is provided.
    include_target No If true, the target station's own most recent observation is embedded under target.observation. Default false.
    include_bearing No If true (default), each neighbor includes bearing_deg (0–359, true north) and bearing_compass (16-point label). Set to false to omit these fields.

    Distance calculation: a bounding-box prefilter narrows candidates by latitude/longitude indexes, then the haversine formula computes exact great-circle distance in kilometers. Both distance_km and distance_mi are always returned. Neighbors are always sorted by ascending distance_km regardless of mode.

    Caching: identical requests (same target + parameters) are served from a 60-second server-side cache.

    Example Requests

    # Defaults: 10 km radius, 10 min freshness, up to 25 neighbors, bearing on
    GET /api/v2/nearby.php?station_code=KMDW
    
    # Wider sweep with longer freshness window
    GET /api/v2/nearby.php?station_code=KMDW&radius_km=50&max_age_minutes=60
    
    # Imperial radius
    GET /api/v2/nearby.php?station_code=KORD&radius_mi=20&max_age_minutes=30
    
    # Top 5 nearest stations only
    GET /api/v2/nearby.php?station_code=KATL&radius_km=100&limit=5&max_age_minutes=60
    
    # Include target's own observation
    GET /api/v2/nearby.php?station_code=KMDW&radius_km=25&include_target=true&max_age_minutes=60
    
    # Explicit list mode — look up specific stations
    GET /api/v2/nearby.php?station_code=KMDW&neighbors=KORD,KDPA,KPWK,KGYY&max_age_minutes=60

    Example Response (Radius Mode)

    {
        "target": {
            "station_code": "KMDW",
            "station_name": "Chicago Midway International Airport",
            "city": "Chicago",
            "state": "IL",
            "country": "US",
            "elevation_m": 188.0,
            "site_type": "airport",
            "timezone": "America/Chicago",
            "latitude": 41.786,
            "longitude": -87.7524
        },
        "filters": {
            "mode": "radius",
            "radius_km": 25,
            "radius_mi": 15.53,
            "max_age_minutes": 60,
            "limit": 25,
            "include_target": false,
            "include_bearing": true,
            "requested_neighbors": null
        },
        "count": 2,
        "neighbors": [
            {
                "station_code": "KORD",
                "station_name": "Chicago O'Hare International Airport",
                "city": "Chicago O'Hare",
                "state": "IL",
                "country": "US",
                "elevation_m": 201.0,
                "site_type": "airport",
                "timezone": "America/Chicago",
                "latitude": 41.9786,
                "longitude": -87.9047,
                "distance_km": 24.4,
                "distance_mi": 15.16,
                "age_seconds": 184,
                "bearing_deg": 323,
                "bearing_compass": "NW",
                "observation": {
                    "observation_time": "2026-05-19 04:12:00",
                    "temperature_c": 19.0,
                    "temperature_f": 66.2,
                    "dew_point_c": 13.0,
                    "dew_point_f": 55.4,
                    "previous_temperature_c": 18.9,
                    "previous_temperature_f": 66.02,
                    "wind_direction": "WSW",
                    "wind_speed_kt": 11.0,
                    "wind_speed_mph": 12.66,
                    "wind_speed_mps": 5.66,
                    "wind_speed_kmh": 20.37,
                    "wind_gust_kt": null,
                    "wind_gust_mph": null,
                    "wind_gust_mps": null,
                    "wind_gust_kmh": null,
                    "relative_humidity": 68.42
                }
            },
            {
                "station_code": "KPWK",
                "...": "(truncated for brevity)"
            }
        ]
    }

    Response Fields

    Top-Level Structure

    FieldTypeDescription
    targetobjectCatalog metadata for the target station. Includes observation sub-object only when include_target=true.
    filtersobjectEcho of how the request was resolved (defaults filled in). Useful for confirming what the server actually used.
    countintegerNumber of neighbors returned (zero is valid).
    neighborsarrayStations matching the query, sorted by ascending distance_km.

    target

    Catalog metadata for the station you queried. Always present (404 returned if the station does not exist or has no coordinates).

    FieldTypeDescription
    station_codestringEchoed station identifier.
    station_namestring|nullFull station name from catalog.
    citystring|nullCity. May be null for mesonet/CWOP stations.
    statestring|nullState/region.
    countrystring|nullISO country (e.g., US).
    elevation_mdecimal|nullElevation in meters.
    site_typestring|nulle.g. airport, mesonet, cwop.
    timezonestring|nullIANA time zone (e.g., America/Chicago).
    latitudedecimalDecimal degrees.
    longitudedecimalDecimal degrees.
    age_secondsinteger|nullOnly present when include_target=true. Seconds since the target's most recent observation.
    observationobject|nullOnly present when include_target=true. Same shape as a neighbor's observation (see below).

    neighbors[]

    Each entry includes catalog metadata, distance/bearing information, and the latest observation.

    FieldTypeDescription
    station_codestringStation identifier.
    station_namestring|nullFull station name.
    city, state, countrystring|nullCatalog location metadata. Mesonet/CWOP stations may have nulls.
    elevation_mdecimal|nullElevation in meters.
    site_typestring|nullCatalog site type.
    timezonestring|nullIANA time zone.
    latitude, longitudedecimalDecimal degrees.
    distance_kmdecimalGreat-circle distance to the target, kilometers.
    distance_midecimalGreat-circle distance to the target, miles.
    bearing_degintegerCompass bearing from target to neighbor, 0–359 degrees from true north. Only present when include_bearing=true (default).
    bearing_compassstring16-point compass label (N, NNE, NE, …). Only present when include_bearing=true.
    age_secondsintegerSeconds since this neighbor's most recent observation.
    observationobjectThe latest observation. See fields below.

    neighbors[].observation

    All temperatures are returned in both Celsius and Fahrenheit. Wind speed and gust are returned in four units (knots, mph, m/s, km/h). Source values are assumed to be stored in knots per METAR/ICAO convention.

    FieldTypeDescription
    observation_timestringUTC timestamp of the observation, YYYY-MM-DD HH:MM:SS.
    temperature_cdecimal|nullAir temperature in Celsius. null if missing, out of sane range (-60 to 60 °C), or sentinel.
    temperature_fdecimal|nullAir temperature in Fahrenheit.
    dew_point_cdecimal|nullDew point in Celsius. Nulled if greater than temperature (physically impossible) plus tolerance.
    dew_point_fdecimal|nullDew point in Fahrenheit.
    previous_temperature_cdecimal|nullThe prior observed temperature for this station, Celsius. Useful for delta calculations.
    previous_temperature_fdecimal|nullSame in Fahrenheit.
    wind_directionstring|nullCompass direction (e.g., WSW, NNE) or VRB for variable.
    wind_speed_ktdecimal|nullWind speed in knots.
    wind_speed_mphdecimal|nullWind speed in miles per hour.
    wind_speed_mpsdecimal|nullWind speed in meters per second.
    wind_speed_kmhdecimal|nullWind speed in kilometers per hour.
    wind_gust_ktdecimal|nullPeak gust in knots. Nulled if less than steady wind speed (data error).
    wind_gust_mphdecimal|nullPeak gust in miles per hour.
    wind_gust_mpsdecimal|nullPeak gust in meters per second.
    wind_gust_kmhdecimal|nullPeak gust in kilometers per hour.
    relative_humiditydecimal|nullComputed from temperature and dew point using the August-Roche-Magnus formula. Percent (0–100).

    filters

    A faithful echo of how the request resolved. Fields are null when not applicable (e.g., radius_km is null in explicit-list mode).

    FieldTypeDescription
    modestringEither radius or explicit_list.
    radius_km, radius_midecimal|nullEffective radius (both units). Null in explicit-list mode.
    max_age_minutesintegerFreshness cutoff applied.
    limitinteger|nullResult cap applied. Null in explicit-list mode.
    include_targetbooleanWhether the target's own observation was embedded.
    include_bearingbooleanWhether bearing fields were included on neighbors.
    requested_neighborsarray|nullThe deduplicated, normalized neighbor list. Null in radius mode.

    Client Examples

    cURL — Default Radius Search

    curl -G "https://wethr.net/api/v2/nearby.php" \
      -H "Authorization: Bearer <YOUR_API_KEY>" \
      --data-urlencode "station_code=KMDW"

    cURL — Wider Radius with Freshness Window

    curl -G "https://wethr.net/api/v2/nearby.php" \
      -H "Authorization: Bearer <YOUR_API_KEY>" \
      --data-urlencode "station_code=KORD" \
      --data-urlencode "radius_km=50" \
      --data-urlencode "max_age_minutes=60" \
      --data-urlencode "limit=10"

    cURL — Explicit Neighbor List

    curl -G "https://wethr.net/api/v2/nearby.php" \
      -H "Authorization: Bearer <YOUR_API_KEY>" \
      --data-urlencode "station_code=KMDW" \
      --data-urlencode "neighbors=KORD,KDPA,KPWK,KGYY" \
      --data-urlencode "max_age_minutes=60"

    Python — Detect Temperature Outliers

    import requests
    import statistics
    
    api_key = 'YOUR_API_KEY'
    headers = {'Authorization': f'Bearer {api_key}'}
    
    resp = requests.get(
        'https://wethr.net/api/v2/nearby.php',
        params={
            'station_code': 'KMDW',
            'radius_km': 50,
            'max_age_minutes': 60,
            'include_target': 'true'
        },
        headers=headers
    )
    data = resp.json()
    
    # Collect every available temperature in the cluster
    temps = []
    if data['target'].get('observation', {}).get('temperature_f') is not None:
        temps.append(('KMDW (target)', data['target']['observation']['temperature_f']))
    for n in data['neighbors']:
        t = n.get('observation', {}).get('temperature_f')
        if t is not None:
            temps.append((n['station_code'], t))
    
    if len(temps) >= 3:
        values = [t for _, t in temps]
        median = statistics.median(values)
        stdev  = statistics.stdev(values)
        print(f"Cluster median: {median:.1f}°F  stdev: {stdev:.2f}")
        for code, t in temps:
            flag = " ← outlier" if abs(t - median) > 2 * stdev else ""
            print(f"  {code:8s} {t:5.1f}°F{flag}")

    JavaScript — Find Closest Reporting Neighbor

    const params = new URLSearchParams({
        station_code: 'KMDW',
        radius_km: '50',
        max_age_minutes: '30',
        limit: '1'
    });
    
    fetch(`/api/v2/nearby.php?${params}`, {
        headers: { 'Authorization': `Bearer ${apiKey}` }
    })
    .then(res => res.json())
    .then(data => {
        if (data.count === 0) {
            console.log('No nearby station reported recently enough.');
            return;
        }
        const n = data.neighbors[0];
        console.log(
            `Closest fresh: ${n.station_code} ` +
            `(${n.city || 'unknown'}) — ` +
            `${n.distance_km.toFixed(1)} km ${n.bearing_compass}, ` +
            `${n.age_seconds}s old, ` +
            `${n.observation.temperature_f}°F, ` +
            `wind ${n.observation.wind_direction || '?'} ${n.observation.wind_speed_kt || 0} kt`
        );
    });

    Pacing API NEW BETA DEV+

    GET /api/v2/pacing.php
    🔒 Developer & Enterprise Only: This endpoint requires a Developer or Enterprise tier API key. Professional and Testing tier keys will receive a 403 Forbidden response.
    ⚠️ Beta Notice: This API is currently in beta testing. While functional, you may encounter bugs, response shape changes, or unexpected behavior as we iterate. Default parameter values, in particular, may be revised. Please report any issues to help us improve the service.

    Returns pacing data for a station's contract day: how the most recent actual observation is running versus each weather model's forecast at the same moment. This is the same calculation shown on the Wethr.net dashboard, computed server-side and exposed as a single key-authenticated call.

    For each model, pacing is defined as actual − model, evaluated at the time of the most recent observation. The model's forecast temperature is linearly interpolated to that exact moment (between the two bracketing forecast points), so the comparison is like-for-like rather than snapped to the nearest forecast hour. A positive pace means actuals are running warmer than the model; a negative pace means cooler.

    Because a single observation resolves to a range of possible true temperatures, pace is reported as a low/high pair (pace_low, pace_high) plus a midpoint (pace) and a human-readable pace_string. The range derives from the precision of the underlying METAR reading: a whole-degree Celsius observation carries an implied uncertainty of ±0.5°C, while a tenths-precision (T-group) reading is treated as exact. The endpoint converts that uncertainty band into the station's reporting unit and keeps the integer degrees that fall within it. For a US station, a 26°C reading becomes 25.5–26.5°C77.9–79.7°Flowest_possible: 78, highest_possible: 79. When only one integer degree falls inside the band, the range collapses to that single value (lowest_possible == highest_possible). This matches the range calculation used on the Wethr.net dashboard.

    📐 Contract-day window & the mode parameter:

    Pacing is always measured within a single contract day. The mode parameter controls how that day's boundaries are drawn, mirroring the resolution sources on the dashboard. nws (default) follows Standard Time year-round, shifting the window +1h during DST so it continues to align with NWS climate-day boundaries (this shift is surfaced as nws_dst_shift in the response). wunderground follows local clock time year-round. The date, when omitted, resolves to "today" in the station's own timezone.

    Endpoint & Parameters

    ParameterRequiredDescription
    station_code Yes Target station identifier. Must exist in the station catalog with a valid timezone. Examples: KTPA, KMDW, EGLC.
    date No Contract day in YYYY-MM-DD format, interpreted in the station's local time. Defaults to the current day in the station's timezone.
    mode No Contract-day window convention: nws (default, Standard Time year-round with a +1h DST shift) or wunderground (local clock time year-round). See the note above.
    models No Comma-separated list of model names to restrict the response to (max 40). Omitted = all available models. Example: GFS,HRRR,ECMWF-IFS. Include NWS in the list to keep the NWS row when filtering.
    include_nws No If true (default), an NWS pacing row is added, computed from the latest NWS hourly forecast. Set to false to omit it.

    Available models: AROME-HD-15, ARPEGE, ARPEGE-EU, ECMWF-HRES, ECMWF-IFS, GEFS, GEM-GDPS, GEM-HRDPS, GFS, GFS-MOS, HRRR, ICON, JMA, LAV-MOS, NAM, NAM-MOS, NAM4KM, NBM, NBS-MOS, RAP, RRFS, UKMO, plus the synthetic NWS row. Only models with a forecast run covering the contract day appear in a given response; the model set returned therefore varies by station and day.

    Reference observation: pacing is anchored to the most recent observation within the contract-day window that is at or before the current time. If no such observation exists yet (e.g., early in the contract day), reference_observation is null, every model's pace fields are null, and a top-level note explains why. Model run times are still listed in that case.

    Units: US stations report in Fahrenheit (unit: "F"); international stations report in their native Celsius (unit: "C"). All temperatures and pace values in a response use the station's native unit.

    NWS row normalization: the NWS row is placed on the same observation-time, full-range basis as every other model so all rows are directly comparable. (The dashboard's NWS display uses a slightly different reference point; this endpoint normalizes it for consistency.)

    Caching: identical requests (same station + parameters) are served from a 60-second server-side cache.

    Example Requests

    # Defaults: today in the station's timezone, NWS mode, all models, NWS row on
    GET /api/v2/pacing.php?station_code=KTPA
    
    # Specific contract day
    GET /api/v2/pacing.php?station_code=KMDW&date=2026-05-29
    
    # Wunderground (local-clock) window convention
    GET /api/v2/pacing.php?station_code=KMDW&mode=wunderground
    
    # Restrict to a few models, drop the NWS row
    GET /api/v2/pacing.php?station_code=KTPA&models=GFS,HRRR,ECMWF-IFS&include_nws=false

    Example Response

    {
        "station": {
            "station_code": "KTPA",
            "station_name": "TAMPA",
            "city": "Tampa",
            "state": "FL",
            "country": "US",
            "elevation_m": 11,
            "site_type": null,
            "timezone": "America/New_York",
            "latitude": 27.9619,
            "longitude": -82.5403
        },
        "contract_day": {
            "date": "2026-05-29",
            "mode": "nws",
            "unit": "F",
            "is_metric": false,
            "window_start_utc": "2026-05-29 05:00:00",
            "window_end_utc": "2026-05-30 05:00:00",
            "nws_dst_shift": true
        },
        "reference_observation": {
            "observation_time_utc": "2026-05-29 20:24:00",
            "observation_time_local": "2026-05-29 16:24:00",
            "local_hour": 16.4,
            "temperature": 87.8,
            "lowest_possible": 87,
            "highest_possible": 88,
            "precision_level": null,
            "unit": "F"
        },
        "filters": {
            "requested_models": null,
            "include_nws": true
        },
        "count": 20,
        "models": {
            "GFS": {
                "run_time_utc": "2026-05-29 12:00:00",
                "model_temp": 89.4,
                "pace_low": -2.4,
                "pace_high": -1.4,
                "pace": -1.9,
                "pace_string": "-2.4 to -1.4"
            },
            "HRRR": {
                "run_time_utc": "2026-05-29 19:00:00",
                "model_temp": 87.5,
                "pace_low": -0.5,
                "pace_high": 0.5,
                "pace": 0,
                "pace_string": "-0.5 to +0.5"
            },
            "ECMWF-IFS": {
                "run_time_utc": "2026-05-29 12:00:00",
                "model_temp": 86.7,
                "pace_low": 0.3,
                "pace_high": 1.3,
                "pace": 0.8,
                "pace_string": "+0.3 to +1.3"
            },
            "NWS": {
                "run_time_utc": null,
                "version": 5,
                "model_temp": 89,
                "pace_low": -2,
                "pace_high": -1,
                "pace": -1.5,
                "pace_string": "-2.0 to -1.0"
            }
        }
    }

    Truncated for brevity — the full models map contains one entry per available model (here, count: 20). Every model row has the identical shape shown for GFS above; only the NWS row differs, carrying a version field and a null run_time_utc.

    Response Fields

    station

    Catalog metadata for the requested station.

    FieldTypeDescription
    station_codestringStation identifier.
    station_namestringFull station name.
    city, state, countrystring|nullLocation descriptors.
    elevation_mdecimal|nullElevation in meters.
    site_typestring|nullSite classification, when available.
    timezonestringIANA timezone used to resolve the contract day.
    latitude, longitudedecimal|nullCoordinates.

    contract_day

    The window the pacing was computed over.

    FieldTypeDescription
    datestringContract day (YYYY-MM-DD), in the station's local time.
    modestringWindow convention applied: nws or wunderground.
    unitstringTemperature unit for this response: F or C.
    is_metricbooleanWhether the station reports in metric (Celsius).
    window_start_utcdatetimeInclusive start of the contract-day window (UTC).
    window_end_utcdatetimeExclusive end of the contract-day window (UTC).
    nws_dst_shiftbooleanWhether the +1h NWS DST shift was applied to the window.

    reference_observation

    The most recent actual observation that pacing is measured against. null when no observation is available in the window yet (see note).

    FieldTypeDescription
    observation_time_utcdatetimeObservation timestamp (UTC).
    observation_time_localdatetimeObservation timestamp in the station's local time.
    local_hourdecimalFractional local hour of the observation (e.g., 16.4 = 16:24). This is the moment each model forecast is interpolated to.
    temperaturedecimalObserved temperature in the station's native unit.
    lowest_possibleintegerLowest possible true temperature implied by the reading's precision. Lower bound of the pace range.
    highest_possibleintegerHighest possible true temperature implied by the reading's precision. Upper bound of the pace range.
    precision_levelinteger|nullPrecision indicator for the reading. 1 denotes an exact-precision value (lowest_possible == highest_possible); null denotes a whole-degree reading resolved to a range.
    unitstringUnit of the observation: F or C.

    models

    A map keyed by model name. Each entry reports the interpolated model temperature and the resulting pace. When a model has no forecast points within the window, its model_temp and all pace fields are null (the run_time_utc is still listed).

    FieldTypeDescription
    run_time_utcdatetime|nullModel run the forecast came from (UTC). Always null for the NWS row.
    versionintegerNWS row only. Version of the NWS hourly forecast used.
    model_tempdecimal|nullModel forecast temperature interpolated to the observation's local_hour, in the station's native unit.
    pace_lowdecimal|nulllowest_possible − model_temp.
    pace_highdecimal|nullhighest_possible − model_temp.
    pacedecimal|nullMidpoint of pace_low and pace_high.
    pace_stringstring|nullHuman-readable pace, signed (e.g., "+0.3 to +1.3", or a single value when low and high coincide).

    filters & count

    FieldTypeDescription
    filters.requested_modelsarray|nullThe normalized model filter applied. null when all models were returned.
    filters.include_nwsbooleanWhether the NWS row was requested.
    countintegerNumber of model rows in models.
    notestringPresent only when no reference observation was available; explains why pace values are null.

    Client Examples

    cURL — Default (All Models, Today)

    curl -G "https://wethr.net/api/v2/pacing.php" \
      -H "Authorization: Bearer <YOUR_API_KEY>" \
      --data-urlencode "station_code=KTPA"

    cURL — Specific Day, Selected Models

    curl -G "https://wethr.net/api/v2/pacing.php" \
      -H "Authorization: Bearer <YOUR_API_KEY>" \
      --data-urlencode "station_code=KMDW" \
      --data-urlencode "date=2026-05-29" \
      --data-urlencode "models=GFS,HRRR,ECMWF-IFS" \
      --data-urlencode "include_nws=false"

    Python — Rank Models by How Warm/Cool They Are Running

    import requests
    
    api_key = 'YOUR_API_KEY'
    headers = {'Authorization': f'Bearer {api_key}'}
    
    resp = requests.get(
        'https://wethr.net/api/v2/pacing.php',
        params={'station_code': 'KTPA'},
        headers=headers
    )
    data = resp.json()
    
    ref = data.get('reference_observation')
    if not ref:
        print(data.get('note', 'No pacing available yet.'))
    else:
        unit = data['contract_day']['unit']
        print(f"Reference: {ref['temperature']}°{unit} "
              f"at {ref['observation_time_local']} "
              f"(range {ref['lowest_possible']}–{ref['highest_possible']})")
    
        # Rank by midpoint pace: most positive = actuals warmest vs model
        rows = [(name, m['pace']) for name, m in data['models'].items()
                if m.get('pace') is not None]
        rows.sort(key=lambda r: r[1], reverse=True)
    
        print("\nModels running COOLEST vs actuals (actuals warmer) at top:")
        for name, pace in rows:
            sign = '+' if pace >= 0 else ''
            print(f"  {name:12s} {sign}{pace:.1f}°{unit}")

    JavaScript — Show the Consensus Pace

    const params = new URLSearchParams({ station_code: 'KTPA' });
    
    fetch(`/api/v2/pacing.php?${params}`, {
        headers: { 'Authorization': `Bearer ${apiKey}` }
    })
    .then(res => res.json())
    .then(data => {
        if (!data.reference_observation) {
            console.log(data.note || 'No pacing available yet.');
            return;
        }
        const unit = data.contract_day.unit;
        const paces = Object.values(data.models)
            .map(m => m.pace)
            .filter(p => p !== null);
    
        if (paces.length === 0) {
            console.log('No model paces to summarize.');
            return;
        }
        const avg = paces.reduce((a, b) => a + b, 0) / paces.length;
        const verdict = avg > 0 ? 'WARMER than models'
                      : avg < 0 ? 'COOLER than models'
                      : 'on pace with models';
        console.log(
            `${data.station.station_code}: actuals running ` +
            `${avg.toFixed(1)}°${unit} ${verdict} ` +
            `(consensus of ${paces.length} models)`
        );
    });
  • ⚠️ API Version 1 (Deprecated) — This version is deprecated and frozen: it will not receive new fields, data, or features. It remains available and operational so existing integrations keep working and is not being taken down. New development should use API v2, which is actively maintained and offers expanded features, additional data fields, and broader station support. Base URL: https://wethr.net/api/v1/

    Introduction Deprecated

    The Wethr.net API v1 provides access to weather observation data and model forecasts. This version supports 24 US stations with all temperatures in Fahrenheit.

    Base URL: https://wethr.net/api/v1/


    Authentication Deprecated

    Include your API key in the request header:

    Authorization: Bearer <YOUR_API_KEY>

    Or:

    X-API-Key: <YOUR_API_KEY>

    Rate Limiting Deprecated

    TierRequests/MinuteRequests/Day
    Professional605,000
    Developer30050,000
    Enterprise30050,000

    Error Responses Deprecated

    • 400 Bad Request — Invalid parameters
    • 401 Unauthorized — Invalid API key
    • 429 Too Many Requests — Rate limit exceeded
    • 500 Server Error — Internal error

    Observations API Deprecated

    GET /api/v1/observations.php

    Mode: Latest Observation

    Returns the most recent observation.

    GET /api/v1/observations.php?station_code=KMDW&mode=latest

    Example Response

    {
        "station_code": "KMDW",
        "temperature": 26.7,
        "observation_time": "2025-06-15T18:53:00Z",
        "lowest_probable_f": 80,
        "highest_probable_f": 80,
        "dsm_high_f": 83,
        "relative_humidity": 55.2
    }

    Mode: Wethr High

    Returns the calculated trading-day high. Logic is determined automatically by station type.

    GET /api/v1/observations.php?station_code=KMDW&mode=wethr_high

    Example Response

    {
        "station_code": "KMDW",
        "date": "2025-06-15",
        "wethr_high": 83,
        "time_of_high_utc": "2025-06-15 20:53:00",
        "calculation_logic": "standard"
    }

    Mode: History

    Retrieves observations over a time range (max 24 hours).

    GET /api/v1/observations.php?station_code=KMDW&start_time=2025-06-15T00:00:00Z&end_time=2025-06-15T12:00:00Z

    Observation Data Model

    FieldTypeDescription
    station_codestringStation identifier
    observation_timedatetimeUTC timestamp
    temperaturedecimalTemperature (Celsius)
    temperature_fdecimalTemperature (Fahrenheit)
    lowest_probable_fintegerMin probable temp (°F)
    highest_probable_fintegerMax probable temp (°F)
    dew_pointdecimalDew point (Celsius)
    relative_humiditydecimalRelative humidity (%)
    cli_high / cli_high_fdecimal/intCLI report high
    dsm_high / dsm_high_fdecimal/intDSM report high
    six_hour_highdecimal6-hour high (Celsius)
    wind_speeddecimalWind speed (knots)
    visibilitydecimalVisibility (miles)

    Forecasts API Deprecated

    GET /api/v1/forecasts.php

    Parameters

    ParameterRequiredDescription
    location_nameYesLocation identifier
    start_valid_timeYesStart of range (ISO 8601)
    end_valid_timeYesEnd of range (ISO 8601)
    modelNoModel filter

    Available Models: ARPEGE, ECMWF-IFS, GEFS, GEM-GDPS, GEM-HRDPS, GFS, GFS-MOS, HRRR, ICON, JMA, LAV-MOS, NAM, NAM-MOS, NAM4KM, NBM, NBS-MOS, RAP, UKMO

    Example Request

    GET /api/v1/forecasts.php?location_name=KMDW&start_valid_time=2025-06-15T18:00:00Z&end_valid_time=2025-06-15T20:00:00Z&model=HRRR

    Forecast Data Model

    FieldTypeDescription
    modelstringModel name
    location_namestringLocation
    run_timedatetimeModel run time
    valid_timedatetimeForecast valid time
    forecast_hourintegerLead time (hours)
    temperature_kdecimalTemp (Kelvin)
    temperature_fdecimalTemp (Fahrenheit)
    temperature_cdecimalTemp (Celsius)

    Client Examples Deprecated

    cURL — Latest Observation

    curl -G "https://wethr.net/api/v1/observations.php" \
      -H "Authorization: Bearer <YOUR_API_KEY>" \
      --data-urlencode "station_code=KMDW" \
      --data-urlencode "mode=latest"

    cURL — Wethr High

    curl -G "https://wethr.net/api/v1/observations.php" \
      -H "Authorization: Bearer <YOUR_API_KEY>" \
      --data-urlencode "station_code=KMDW" \
      --data-urlencode "mode=wethr_high"

    cURL — History

    curl -G "https://wethr.net/api/v1/observations.php" \
      -H "Authorization: Bearer <YOUR_API_KEY>" \
      --data-urlencode "station_code=KMDW" \
      --data-urlencode "start_time=2025-06-15T00:00:00Z" \
      --data-urlencode "end_time=2025-06-15T12:00:00Z"

    cURL — Forecasts

    curl -G "https://wethr.net/api/v1/forecasts.php" \
      -H "Authorization: Bearer <YOUR_API_KEY>" \
      --data-urlencode "location_name=KMDW" \
      --data-urlencode "start_valid_time=2025-06-15T18:00:00Z" \
      --data-urlencode "end_valid_time=2025-06-15T20:00:00Z" \
      --data-urlencode "model=HRRR"
Top