{"components":{"parameters":{"HourlyIndexCityID":{"description":"Kalshi index city ID (see `GET /api/v1/hourly-indexes`).","example":"miami","in":"path","name":"city","required":true,"schema":{"type":"string"}},"HourlyIndexForecastModel":{"description":"Forecast model of the index forecast. Defaults to HRRR. RRFS is a separate forecast with\nits own bias, history and accuracy, never a fallback for HRRR or the reverse.\n","in":"query","name":"model","schema":{"default":"ncep_hrrr_conus_15min","enum":["ncep_hrrr_conus_15min","ncep_rrfs_conus_15min"],"type":"string"}}},"schemas":{"City":{"example":{"id":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","name":"New York City","slug":"nyc","state":"NY","stations":[{"ibkr_active":true,"id":"b2c3d4e5-f6a7-8901-bcde-f12345678901","kalshi_active":true,"latitude":40.781578,"longitude":-73.950516,"name":"New York City Central Park","polymarket_active":false,"polymarket_us_active":true,"robinhood_active":false,"station_id":"KNYC"},{"ibkr_active":false,"id":"c3d4e5f6-a7b8-9012-cdef-123456789012","kalshi_active":false,"latitude":40.7772,"longitude":-73.8726,"name":"LaGuardia Airport","polymarket_active":true,"polymarket_us_active":false,"robinhood_active":true,"station_id":"KLGA"}],"timezone":"America/New_York"},"properties":{"id":{"format":"uuid","type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"state":{"type":"string"},"stations":{"items":{"$ref":"#/components/schemas/Station"},"type":"array"},"timezone":{"type":"string"}},"type":"object"},"CursorPagination":{"properties":{"limit":{"example":500,"maximum":5000,"minimum":1,"type":"integer"},"next_cursor":{"description":"Opaque cursor for the next page. Null when there are no more rows.","nullable":true,"type":"string"}},"type":"object"},"DataAccess":{"description":"Where the forecast data files are and the tokens that open the ones the caller's plan may read.\nA file's URL is `base_url` + its path + `?` + the `query` of the grant whose `prefix` the path\nstarts with. Treat `query` as an opaque string. Paths under the `public` prefixes need no query.\n","properties":{"base_url":{"example":"https://data.minutetemp.com","type":"string"},"chart":{"allOf":[{"$ref":"#/components/schemas/DataAccessGrant"}],"description":"Grant for the per-station chart files of the MinuteTemp web app, whose format is not\npart of this API (see **Getting forecast data**). Absent when the plan has no file\naccess, and whenever the chart files are not confirmed current.\n"},"expires_at":{"description":"When the tokens in this response stop working (at most one hour after they were issued).\nFetch a new response shortly before it. Absent when the response carries no tokens.\n","format":"date-time","type":"string"},"forecasts":{"description":"One grant per UTC date the plan may read forecast run files for: from the start of the\nplan's history window (`max_history_days`) through tomorrow, oldest first. A run's date\nis the UTC date it was fetched. Empty when the plan has no file access.\n","items":{"allOf":[{"$ref":"#/components/schemas/DataAccessGrant"},{"properties":{"date":{"example":"2026-10-12","format":"date","type":"string"}},"required":["date"],"type":"object"}]},"type":"array"},"hourly_index_days":{"description":"One grant per UTC date the plan may read hourly index day files for, under\n`/private/v1/hourly-index-days/YYYY/MM/DD/`: from the start of the plan's history window\n(`max_history_days`) through today, oldest first. A day file holds the steps of one UTC\ndate. Absent when the plan has no file access or while hourly index files are not\noffered.\n","items":{"allOf":[{"$ref":"#/components/schemas/DataAccessGrant"},{"properties":{"date":{"example":"2026-10-12","format":"date","type":"string"}},"required":["date"],"type":"object"}]},"type":"array"},"hourly_indexes":{"allOf":[{"$ref":"#/components/schemas/DataAccessGrant"}],"description":"Grant for the current hourly index files, one per index city, under\n`/private/v1/hourly-indexes/`. Absent when the plan has no file access, while hourly\nindex files are not offered, and whenever the files are not confirmed current.\nSee **Hourly index data files**.\n"},"public":{"description":"Prefixes of the files that need no token.","properties":{"chart_prefix":{"example":"/public/v1/chart/","type":"string"},"forecasts_prefix":{"example":"/public/v1/forecasts/","type":"string"},"hourly_indexes_prefix":{"description":"Public hourly index files of the cities without a Kalshi series. Absent while hourly index files are not offered.","example":"/public/v1/hourly-indexes/","type":"string"}},"required":["chart_prefix","forecasts_prefix"],"type":"object"}},"required":["base_url","forecasts","public"],"type":"object"},"DataAccessGrant":{"properties":{"prefix":{"description":"Every file path starting with this prefix is covered by `query`.","example":"/private/v1/chart/","type":"string"},"query":{"description":"Opaque token. Append it to the file URL as the whole query string.","example":"1760288400-c2lnbmF0dXJl","type":"string"}},"required":["prefix","query"],"type":"object"},"EffectiveLimits":{"description":"Effective limits for the authenticated API key's subscription tier. Static caps match the\n**Plan Limits** tables in the API overview; this endpoint adds live rate-limit snapshots.\n","properties":{"bucket_capacity":{"description":"Maximum immediate stored token capacity for the user's plan tier.","type":"integer"},"burst_size":{"description":"Maximum requests a fully idle client can spend across the next minute for the user's plan tier.","type":"integer"},"ip_guard":{"properties":{"burst":{"type":"integer"},"requests_per_second":{"type":"integer"}},"type":"object"},"max_history_days":{"description":"Maximum calendar-day span for time-bounded history endpoints (market snapshot history,\nforecast run lists, report history, oracle score lookback). Starter 7, Pro 30, Clanker 90.\n","example":30,"type":"integer"},"rate_limit_remaining":{"description":"Immediate stored tokens remaining in the request-serving region, after counting this request, if available.","nullable":true,"type":"integer"},"rate_limit_reset_seconds":{"description":"Seconds until the immediate token bucket fully refills in the request-serving region, if available.","nullable":true,"type":"integer"},"requests_per_minute":{"description":"Sustained per-minute rate limit (Starter 15, Pro 100, Clanker 250).","type":"integer"},"served_by_region":{"description":"Runtime region identifier for the service that served this request, when available.","nullable":true,"type":"string"},"tier":{"description":"Paid subscription tier for this API key.","enum":["starter","pro","clanker"],"type":"string"},"ws_subscriptions":{"description":"Per-user subscription limits on the API-key WebSocket (`/ws/api/*`), across all of the\nuser's connections. See **Plan Limits**.\n","properties":{"market":{"description":"Market subscription limit: cities, market stations and hourly indexes.\nStarter 6, Pro 60, Clanker 200. `null` means unlimited.\n","example":60,"nullable":true,"oneOf":[{"type":"integer"},{"enum":[null]}],"x-go-type":"int"},"neighbour":{"description":"Neighbour subscription limit: observation-only stations not listed by\n`GET /api/v1/stations`. Starter 30, Pro 300, Clanker unlimited (`null`).\n","example":300,"nullable":true,"oneOf":[{"type":"integer"},{"enum":[null]}],"x-go-type":"int"}},"type":"object"}},"type":"object"},"Error":{"properties":{"error":{"properties":{"code":{"example":"STATION_NOT_FOUND","type":"string"},"message":{"example":"Station with ID 'xyz' was not found","type":"string"},"request_id":{"example":"req_abc123","type":"string"}},"required":["code","message"],"type":"object"}},"required":["error"],"type":"object"},"ForecastBundle":{"description":"Forecast data grouped by model and run","properties":{"forecast_run":{"properties":{"fetched_at":{"format":"date-time","type":"string"},"id":{"type":"string"},"timezone":{"example":"America/Chicago","type":"string"},"utc_offset_seconds":{"type":"integer"}},"type":"object"},"hourly":{"items":{"$ref":"#/components/schemas/HourlyForecast"},"type":"array"},"model_id":{"example":"ncep_hrrr_conus","type":"string"}},"type":"object"},"ForecastModel":{"properties":{"id":{"example":"ncep_hrrr_conus","type":"string"},"is_public":{"description":"True for models available on the free web UI without a paid subscription\n(`nws_gridpoint`, `ncep_nbm_conus`, `gfs_global`). Paid API subscribers receive all models.\n","type":"boolean"},"max_forecast":{"description":"Maximum forecast hours","example":48,"type":"integer"},"name":{"example":"HRRR CONUS","type":"string"},"provider":{"example":"NCEP","type":"string"},"resolution":{"example":"3km","type":"string"}},"type":"object"},"ForecastRunFile":{"description":"A forecast data file: one forecast run with all its steps (schema\n`minutetemp.forecast_run/v1`). It is served by the data host, not by this API; see\n**Forecast data files**.\n","properties":{"count":{"description":"Number of entries in `hourly`.","type":"integer"},"hourly":{"items":{"$ref":"#/components/schemas/ForecastRunFilePoint"},"type":"array"},"run":{"properties":{"backfilled":{"description":"True when the file was written some time after the run was fetched.","type":"boolean"},"data_hash":{"type":"string"},"fetched_at":{"format":"date-time","type":"string"},"forecast_time":{"format":"date-time","type":"string"},"id":{"format":"uuid","type":"string"},"model_id":{"example":"ncep_hrrr_conus","type":"string"},"revision":{"description":"Always 1.","type":"integer"},"station_code":{"example":"KMDW","type":"string"},"station_id":{"format":"uuid","type":"string"},"step_seconds":{"example":3600,"type":"integer"},"timezone":{"example":"America/Chicago","type":"string"},"utc_offset_seconds":{"type":"integer"}},"required":["id","revision","station_id","station_code","model_id","forecast_time","fetched_at","timezone","utc_offset_seconds","data_hash","step_seconds","backfilled"],"type":"object"},"schema":{"example":"minutetemp.forecast_run/v1","type":"string"},"units":{"additionalProperties":{"type":"string"},"description":"Unit of each weather field of `hourly`, by field name. Fields without a unit have no entry.","example":{"temperature_2m_c":"C","temperature_2m_f":"F","wind_speed_10m":"km/h"},"type":"object"}},"required":["schema","run","units","count","hourly"],"type":"object"},"ForecastRunFilePoint":{"description":"One forecast step. The two temperatures are always present, `null` when the model gave\nnone. Every other weather field is present only when it has a value; the fields listed here\nare the ones with a unit in `units`, and a file may carry others (for example\n`weather_code`, `surface_pressure`, `is_day`).\n","properties":{"apparent_temperature_c":{"type":"number"},"apparent_temperature_f":{"type":"number"},"cloud_cover":{"type":"number"},"dew_point_2m":{"type":"number"},"model_id":{"description":"Present only when the step comes from another model than the run's.","type":"string"},"precipitation":{"type":"number"},"precipitation_probability":{"type":"number"},"pressure_msl":{"type":"number"},"relative_humidity_2m":{"type":"number"},"temperature_2m_c":{"type":["number","null"]},"temperature_2m_f":{"type":["number","null"]},"time":{"format":"date-time","type":"string"},"visibility":{"type":"number"},"weather_code":{"type":"integer"},"wind_direction_10m":{"type":"number"},"wind_gusts_10m":{"type":"number"},"wind_speed_10m":{"type":"number"}},"required":["time","temperature_2m_c","temperature_2m_f"],"type":"object"},"ForecastRunSummary":{"properties":{"data_hash":{"type":"string"},"fetched_at":{"format":"date-time","type":"string"},"forecast_time":{"format":"date-time","type":"string"},"id":{"format":"uuid","type":"string"},"model_id":{"example":"ncep_hrrr_conus","type":"string"},"object_path":{"description":"Path of the run's file on the data host (see **Forecast data files**). The file's URL is\nthe `base_url` of `GET /api/v1/data/access` + this path + `?` + the `query` of the grant\nfor the run's fetch date. Absent for a run that has no file. See **Getting forecast\ndata** for the full flow.\n","example":"/private/v1/forecasts/2026/10/12/KMDW/ncep_hrrr_conus/0199d2a0-0000-7000-8000-000000000001.json","type":"string"},"station_id":{"format":"uuid","type":"string"},"timezone":{"example":"America/Chicago","type":"string"},"utc_offset_seconds":{"type":"integer"}},"type":"object"},"HourlyForecast":{"description":"Slim forecast point returned by the external `/api/v1/*` forecast routes.\nTemperature fields are the primary guaranteed values when a point is present. Secondary\nweather fields may be null or omitted when the upstream model did not return them or when\nthat station capability did not request them.\n","properties":{"apparent_temperature_f":{"nullable":true,"type":"number"},"cloud_cover":{"nullable":true,"type":"number"},"dew_point_2m":{"nullable":true,"type":"number"},"precipitation_probability":{"nullable":true,"type":"number"},"pressure_msl":{"nullable":true,"type":"number"},"relative_humidity_2m":{"nullable":true,"type":"number"},"temperature_2m_c":{"description":"Temperature at 2m in Celsius","nullable":true,"type":"number"},"temperature_2m_f":{"description":"Temperature at 2m in Fahrenheit","nullable":true,"type":"number"},"time":{"format":"date-time","type":"string"},"weather_code":{"nullable":true,"type":"integer"},"wind_direction_10m":{"nullable":true,"type":"number"},"wind_gusts_10m":{"nullable":true,"type":"number"},"wind_speed_10m":{"nullable":true,"type":"number"}},"type":"object"},"HourlyIndexCalibration":{"properties":{"change_reason":{"type":"string"},"city":{"type":"string"},"city_reference_c":{"type":"number"},"config_version":{"type":"string"},"effective_at":{"format":"date-time","type":"string"},"published_at":{"format":"date-time","type":"string"},"stations":{"description":"Members as Kalshi published them (station_id, weight, offset_c, update_note).","items":{"additionalProperties":true,"type":"object"},"type":"array"},"window_end":{"format":"date-time","nullable":true,"type":"string"},"window_start":{"format":"date-time","nullable":true,"type":"string"}},"type":"object"},"HourlyIndexCity":{"description":"A city's current state; also the body of the WebSocket `hourly_index_snapshot`. `seq` is the\nlast event folded in: apply WebSocket events with a higher `seq`. Index-only cities have\n`hourly_series_ticker: null` and an empty `ladder`.\n","properties":{"calibration_pending":{"type":"boolean"},"city":{"example":"miami","type":"string"},"config_version":{"example":"miami-temperature-v1.0-cal-20260928","type":"string"},"contract_terms_url":{"description":"The Kalshi contract terms (PDF) of the city's hourly series, from Kalshi's series listing; null for index-only cities.","example":"https://kalshi-public-docs.s3.amazonaws.com/contract_terms/WINDEX.pdf","nullable":true,"type":"string"},"current_hour":{"allOf":[{"$ref":"#/components/schemas/HourlyIndexHour"}],"nullable":true},"display_name":{"example":"Miami","type":"string"},"forecast":{"allOf":[{"$ref":"#/components/schemas/HourlyIndexForecast"}],"description":"Latest HRRR 15-minute index forecast (null until the first run).","nullable":true},"forecast_station_id":{"example":"KMIA","nullable":true,"type":"string"},"forecasts":{"additionalProperties":{"$ref":"#/components/schemas/HourlyIndexForecast"},"description":"The other forecast models' latest index forecasts, by model ID (today only\n`ncep_rrfs_conus_15min`), each computed like `forecast` from that model's own member\nruns. Absent while no other model has a forecast for the city; a forecast with no step\nleft is not included. `forecast` stays HRRR.\n","type":"object"},"hourly_series_ticker":{"example":"KXTEMPMIAH","nullable":true,"type":"string"},"ladder":{"description":"Quotes for the open event's strikes (list endpoint omits it).","items":{"$ref":"#/components/schemas/HourlyIndexMarketQuote"},"type":"array"},"latest":{"allOf":[{"$ref":"#/components/schemas/HourlyIndexMinute"}],"description":"Newest minute, without member readings. Its value_f is null while a provisional minute is below quorum.","nullable":true},"latest_valued":{"allOf":[{"$ref":"#/components/schemas/HourlyIndexMinute"}],"description":"Newest minute with a value (provisional or official), without member readings. The same minute as latest unless latest has no value.","nullable":true},"members":{"items":{"$ref":"#/components/schemas/HourlyIndexMember"},"type":"array"},"next_event":{"allOf":[{"$ref":"#/components/schemas/HourlyIndexEvent"}],"nullable":true},"open_event":{"allOf":[{"$ref":"#/components/schemas/HourlyIndexEvent"}],"nullable":true},"page_slug":{"type":"string"},"quorum":{"$ref":"#/components/schemas/HourlyIndexQuorum"},"recent_settlements":{"description":"Up to 3 newest settlements, newest first.","items":{"$ref":"#/components/schemas/HourlyIndexSettlement"},"type":"array"},"seq":{"example":48213,"format":"int64","type":"integer"},"timezone":{"example":"America/New_York","type":"string"}},"type":"object"},"HourlyIndexEvent":{"description":"An hourly Kalshi event with its ladder.","properties":{"close_time":{"format":"date-time","nullable":true,"type":"string"},"event_ticker":{"type":"string"},"ladder":{"items":{"$ref":"#/components/schemas/HourlyIndexLadderMarket"},"type":"array"},"open_time":{"format":"date-time","nullable":true,"type":"string"},"series_ticker":{"type":"string"},"status":{"enum":["listed","open","closed"],"type":"string"},"strike_time":{"format":"date-time","type":"string"}},"type":"object"},"HourlyIndexFeedCondition":{"properties":{"codes":{"description":"`member_excluded`: the member's codes (missing, late, range, rate_spatial, extreme); `qc_substitution`: the rejected reading's QC codes (range, rate_spatial, extreme, provider_qc).","items":{"type":"string"},"type":"array"},"contributors":{"description":"`quorum_risk`: contributing members in the newest official minute.","type":"integer"},"last_seen":{"description":"The last affected minute in the window.","format":"date-time","type":"string"},"members":{"description":"`quorum_risk`: all members.","type":"integer"},"minutes":{"description":"Minutes affected.","example":8,"type":"integer"},"severity":{"description":"`critical` for `quorum_risk` and `unavailable_minutes`.","enum":["warning","critical"],"type":"string"},"share":{"description":"minutes / window_minutes, rounded to 3 places.","example":0.267,"type":"number"},"since":{"description":"The first affected minute in the window.","format":"date-time","type":"string"},"station_id":{"description":"The member station; null for `quorum_risk` and `unavailable_minutes`.","example":"KDPA","nullable":true,"type":"string"},"type":{"description":"`persistent_fallback`: a member on its METAR fallback for at least 90% of the window's\nofficial minutes; `intermittent_fallback`: on it for fewer (at least 2 minutes);\n`member_excluded`: a member out of the index with no usable fallback (at least 2\nminutes); `qc_substitution`: a member's 1-minute reading failed QC and its METAR was\nused; `quorum_risk`: the newest official minute has a member out and losing the\nheaviest remaining member would break the published quorum (only for cities with a\npublished quorum); `unavailable_minutes`: final minutes below quorum.\n","enum":["persistent_fallback","intermittent_fallback","member_excluded","qc_substitution","quorum_risk","unavailable_minutes"],"type":"string"},"weight_share":{"description":"`quorum_risk`: the contributing weight share.","type":"number"},"window_minutes":{"description":"Minutes considered (official minutes for member conditions, final minutes for `unavailable_minutes`, 1 for `quorum_risk`).","example":30,"type":"integer"}},"required":["type","severity","station_id","minutes","window_minutes","share","since","last_seen"],"type":"object"},"HourlyIndexForecast":{"description":"The index forecast from the members' latest 15-minute runs of one forecast model\n(`model_id`: HRRR, or RRFS where requested): each step's index computed\nfrom the members' temperatures with Kalshi's formula, the calibration effective at the step\nand the city's quorum. Every per-step array (`value_f`, `contributors`, `weight_share`,\n`quorum_met`, `partial`, `members[].temp_f`) is aligned to `steps`. The first step is the\n15-minute step at or before the refresh (compare it with the latest observation for\nbias); the rest run up to 18 hours ahead. `value_f` is null below quorum; `partial` marks\nsteps where some members had no forecast. Recomputed when a member's run or a calibration\nchanges (about hourly); replace the whole object when a newer `run_time` or `fetched_at`\narrives.\n\nBias-adjusted forecast: `bias_f` is the newest valued index minute\n(`bias_as_of_minute`) minus the forecast interpolated at it, and `adjusted_f[i]` =\n`value_f[i]` + `bias_f` (null where `value_f` is null or there is no bias). The bias\nfollows every new or revised valued minute and calibration recompute (the WebSocket\n`forecast_bias` event); the curve itself changes only with a new run.\n","properties":{"adjusted_f":{"description":"`value_f[i]` + `bias_f`, aligned to `steps`; null where either is null.","items":{"nullable":true,"type":"number"},"type":"array"},"bias_as_of_minute":{"description":"The index minute `bias_f` was measured at.","format":"date-time","nullable":true,"type":"string"},"bias_f":{"description":"Newest valued index minute minus the forecast at that minute (the first step for an earlier minute); null without a valued minute or forecast value there.","nullable":true,"type":"number"},"config_version":{"type":"string"},"contributors":{"items":{"type":"integer"},"type":"array"},"fetched_at":{"format":"date-time","type":"string"},"member_bias":{"additionalProperties":{"nullable":true,"type":"number"},"description":"Per member, its reading at `bias_as_of_minute` minus its forecast temperature there (°F); null when either is missing. Null without a bias.","nullable":true,"type":"object"},"members":{"items":{"$ref":"#/components/schemas/HourlyIndexForecastMember"},"type":"array"},"members_total":{"type":"integer"},"missing_members":{"description":"Members with no forecast in this run.","items":{"type":"string"},"type":"array"},"model_id":{"example":"ncep_hrrr_conus_15min","type":"string"},"partial":{"items":{"type":"boolean"},"type":"array"},"quorum_met":{"items":{"type":"boolean"},"type":"array"},"run_time":{"description":"First step of the newest member run (the 15-minute step it was fetched in).","format":"date-time","type":"string"},"settles":{"description":"The next (up to 6) top-of-hour steps after the refresh.","items":{"$ref":"#/components/schemas/HourlyIndexForecastSettle"},"type":"array"},"steps":{"items":{"format":"date-time","type":"string"},"type":"array"},"value_f":{"items":{"nullable":true,"type":"number"},"type":"array"},"weight_share":{"items":{"type":"number"},"type":"array"}},"type":"object"},"HourlyIndexForecastAccuracy":{"properties":{"city":{"type":"string"},"hours":{"description":"Determined hours scored in the window.","type":"integer"},"leads":{"description":"One entry per lead (60, 30 and 15 minutes before hour_end).","items":{"properties":{"adjusted":{"$ref":"#/components/schemas/HourlyIndexForecastScore"},"lead_minutes":{"enum":[60,30,15],"type":"integer"},"raw":{"$ref":"#/components/schemas/HourlyIndexForecastScore"}},"type":"object"},"type":"array"},"model_id":{"description":"The forecast model scored.","example":"ncep_hrrr_conus_15min","type":"string"},"window":{"enum":["7d","30d"],"type":"string"}},"type":"object"},"HourlyIndexForecastMember":{"properties":{"offset_c":{"type":"number"},"station_id":{"example":"KMIA","type":"string"},"temp_f":{"description":"The forecast model's 15-minute temperature per step, aligned to `steps` (null when missing).","items":{"nullable":true,"type":"number"},"type":"array"},"weight":{"type":"number"}},"type":"object"},"HourlyIndexForecastScore":{"description":"One forecast (bias-adjusted or raw) at one lead. Errors are forecast minus settle (°F).\nScores are null when `n` is 0.\n","properties":{"bias_f":{"description":"Mean signed error (°F); positive means the forecast ran warm.","nullable":true,"type":"number"},"mae_f":{"description":"Mean absolute error (°F).","nullable":true,"type":"number"},"n":{"description":"Hours with a forecast at this lead.","type":"integer"},"strike_match_rate":{"description":"Share of hours whose forecast fell in the settle's strike bracket (N.99 convention), 0-1.","nullable":true,"type":"number"}},"type":"object"},"HourlyIndexForecastSettle":{"description":"Forecast settle of an upcoming hour (the step at its top-of-hour).","properties":{"adjusted_floor_strike":{"description":"Highest N.99 floor strike strictly below `adjusted_value_f`.","example":84.99,"nullable":true,"type":"number"},"adjusted_value_f":{"description":"`value_f` + the forecast's `bias_f`; null without either.","nullable":true,"type":"number"},"hour_end":{"format":"date-time","type":"string"},"implied_floor_strike":{"description":"Highest N.99 floor strike strictly below value_f (Kalshi's hourly ladder convention).","example":83.99,"nullable":true,"type":"number"},"partial":{"type":"boolean"},"quorum_met":{"type":"boolean"},"value_f":{"nullable":true,"type":"number"}},"type":"object"},"HourlyIndexHour":{"description":"The open (`open`) or closed-but-undetermined (`pending`) hour. `settles_now_as` is the last\nofficial minute in the lookback. The `forecast_*` fields come from the latest HRRR index forecast:\n`forecast_bias_f` is the newest valued index minute minus the forecast at that minute\n(linearly interpolated between 15-minute steps; the first step when the minute is\nearlier), applied as a constant to the forecast at `hour_end`.\n","properties":{"determine_by":{"description":"hour_end + 8 min, the latest time the settle is determined.","format":"date-time","type":"string"},"event_ticker":{"nullable":true,"type":"string"},"feed_conditions":{"description":"Degradations of the city's index feed over the final minutes of the 30 minutes ending\nat the newest final minute; empty when the feed is clean. Under Kalshi's methodology a\nmember without its 1-minute reading falls back to its METAR/SPECI (at most 75 minutes\nold) and a QC-rejected reading is substituted the same way; both minutes still count.\nA member with no usable fallback is excluded, and a minute below quorum is unavailable\n(no value, nothing carried forward).\n","items":{"$ref":"#/components/schemas/HourlyIndexFeedCondition"},"type":"array"},"final_at":{"description":"hour_end + 5 min, when the top-of-hour minute is official.","format":"date-time","type":"string"},"forecast_adjusted_distance_to_strike_f":{"description":"forecast_adjusted_settle_f minus forecast_adjusted_nearest_strike_f (°F, signed).","nullable":true,"type":"number"},"forecast_adjusted_floor_strike":{"description":"The \"greater\" strike the adjusted settle implies under the N.99 convention.","nullable":true,"type":"number"},"forecast_adjusted_nearest_strike_f":{"description":"The open event's floor strike nearest the adjusted settle (null for an index-only city).","nullable":true,"type":"number"},"forecast_adjusted_settle_f":{"description":"forecast_settle_f + forecast_bias_f.","nullable":true,"type":"number"},"forecast_adjusted_strike_side":{"description":"`above` when the adjusted settle is above the nearest strike (its \"greater\" market wins), else `below`.","enum":["above","below",null],"nullable":true,"type":"string"},"forecast_bias_f":{"description":"Newest valued index minute minus the HRRR index forecast at that minute.","nullable":true,"type":"number"},"forecast_settle_f":{"description":"The HRRR index forecast at hour_end (raw, not bias-adjusted).","nullable":true,"type":"number"},"high_f":{"nullable":true,"type":"number"},"hour_end":{"format":"date-time","type":"string"},"low_f":{"nullable":true,"type":"number"},"settles_now_as":{"allOf":[{"$ref":"#/components/schemas/HourlyIndexMinuteValue"},{"$ref":"#/components/schemas/HourlyIndexStrikeDistance"},{"properties":{"implied_floor_strike":{"description":"The open event's highest floor strike strictly below `value_f` (the bracket it settles in now); null for an index-only city.","nullable":true,"type":"number"}},"type":"object"}],"nullable":true},"status":{"enum":["open","pending"],"type":"string"}},"type":"object"},"HourlyIndexLadderMarket":{"properties":{"cap_strike":{"nullable":true,"type":"number"},"floor_strike":{"nullable":true,"type":"number"},"result":{"type":"string"},"status":{"type":"string"},"strike_type":{"type":"string"},"ticker":{"type":"string"},"yes_sub_title":{"type":"string"}},"type":"object"},"HourlyIndexMarketQuote":{"description":"One strike of the ladder. Its quote fields are deprecated and always null: since v1.16 the\npublic API serves no market prices.\n","properties":{"cap_strike":{"nullable":true,"type":"number"},"event_ticker":{"type":"string"},"floor_strike":{"nullable":true,"type":"number"},"last_price":{"deprecated":true,"nullable":true,"type":"integer"},"no_ask":{"deprecated":true,"nullable":true,"type":"integer"},"no_bid":{"deprecated":true,"nullable":true,"type":"integer"},"ticker":{"type":"string"},"volume":{"deprecated":true,"nullable":true,"type":"number"},"yes_ask":{"deprecated":true,"nullable":true,"type":"integer"},"yes_bid":{"deprecated":true,"nullable":true,"type":"integer"}},"type":"object"},"HourlyIndexMatchRate":{"properties":{"city":{"type":"string"},"matched":{"description":"Reconciled hours where our settle matched Kalshi's final value.","type":"integer"},"rate":{"description":"matched / reconciled, null when nothing is reconciled.","nullable":true,"type":"number"},"reconciled":{"description":"Hours Kalshi has finalized in the window that we settled ourselves; `incomplete_data` hours settled from Kalshi's value (`settle_source: kalshi_expiration`) are excluded from both counts.","type":"integer"},"window":{"enum":["7d","30d"],"type":"string"}},"type":"object"},"HourlyIndexMember":{"properties":{"offset_c":{"example":0,"type":"number"},"station_id":{"example":"KFLL","type":"string"},"weight":{"example":0.2,"type":"number"}},"type":"object"},"HourlyIndexMinute":{"description":"One city-minute. `value_f` is `official_f` when `phase` is `official`, else `provisional_f`.\nKeep the highest `revision` per minute; `final` minutes never go back to provisional.\n","properties":{"calibration_pending":{"type":"boolean"},"config_version":{"type":"string"},"contributors":{"type":"integer"},"final":{"type":"boolean"},"kalshi_status":{"enum":["normal","degraded","incomplete"],"nullable":true,"type":"string"},"minute":{"format":"date-time","type":"string"},"official_at":{"format":"date-time","nullable":true,"type":"string"},"official_f":{"nullable":true,"type":"number"},"phase":{"enum":["provisional","official","missing"],"type":"string"},"provisional_at":{"format":"date-time","nullable":true,"type":"string"},"provisional_f":{"nullable":true,"type":"number"},"quorum_met":{"type":"boolean"},"receipt_basis":{"description":"Set on Kalshi historical backfill points, which never settle.","nullable":true,"type":"string"},"recomputed_f":{"nullable":true,"type":"number"},"restated":{"description":"Kalshi changed the value after it was official. `official_f` and `official_at` keep the first official value (the one the settlement used); the latest restated value is in `restated_f`, seen at `restated_at`.","type":"boolean"},"restated_at":{"format":"date-time","nullable":true,"type":"string"},"restated_f":{"nullable":true,"type":"number"},"revision":{"type":"integer"},"stations":{"description":"Member readings; only with `include=stations` (and always on the WebSocket).","items":{"$ref":"#/components/schemas/HourlyIndexStationReading"},"type":"array"},"value_f":{"nullable":true,"type":"number"},"weight_share":{"type":"number"}},"type":"object"},"HourlyIndexMinuteValue":{"properties":{"minute":{"format":"date-time","type":"string"},"value_f":{"type":"number"}},"type":"object"},"HourlyIndexQuorum":{"description":"Minimum contributing members and weight share for a minute to count. `published` is false\nfor cities where Kalshi hasn't published a quorum rule; their settlements are informational.\n","properties":{"min_members":{"example":4,"type":"integer"},"min_weight":{"example":0.8,"type":"number"},"published":{"type":"boolean"}},"type":"object"},"HourlyIndexSettlement":{"description":"Our settlement of one hour. `event_ticker` is null for index-only cities; `informational`\nmarks cities without a published quorum rule. `kalshi_expiration_value` and `match` are null\nuntil Kalshi finalizes the market.\n\n`status` is `determined` (the last official, non-backfill minute in\n[hour_end − 60 min, hour_end]), `no_value` (every minute of the window is known and none is\nofficial: Rule 7.1), or `incomplete_data`: our stored minutes have a gap after the last\nofficial minute (listed in `missing_minutes`), so we never settle on an earlier minute. For a\nmarket city, an `incomplete_data` hour takes Kalshi's `expiration_value` as `settle_value_f`\n(with its `winning_floor_strike`) once Kalshi finalizes it, with `settle_source`\n`kalshi_expiration` and `match` null; such hours are in neither side of the match rate. An\nindex-only city's `incomplete_data` hour gets one refetch of its missing minutes in the\ndaily audit and becomes `determined` or `no_value` if that fills the gap.\n","properties":{"determined_at":{"format":"date-time","type":"string"},"event_ticker":{"nullable":true,"type":"string"},"hour_end":{"format":"date-time","type":"string"},"hour_high_f":{"nullable":true,"type":"number"},"hour_low_f":{"nullable":true,"type":"number"},"informational":{"type":"boolean"},"kalshi_expiration_value":{"nullable":true,"type":"number"},"kalshi_finalized_at":{"format":"date-time","nullable":true,"type":"string"},"match":{"nullable":true,"type":"boolean"},"missing_minutes":{"description":"The minutes without a final value in our data (`incomplete_data` only; otherwise null).","items":{"format":"date-time","type":"string"},"nullable":true,"type":"array"},"revision":{"type":"integer"},"settle_minute":{"format":"date-time","nullable":true,"type":"string"},"settle_source":{"description":"Where `settle_value_f` comes from: `computed` from our index minutes, or `kalshi_expiration` when an `incomplete_data` hour took Kalshi's final value.","enum":["computed","kalshi_expiration"],"type":"string"},"settle_value_f":{"nullable":true,"type":"number"},"status":{"enum":["determined","no_value","incomplete_data"],"type":"string"},"winning_floor_strike":{"nullable":true,"type":"number"}},"type":"object"},"HourlyIndexStationReading":{"description":"One member's reading inside a city-minute.","properties":{"change_5m_f":{"description":"This reading minus the station's reading 5 minutes earlier (or at the nearest earlier\nminute with a reading within 10 minutes of that), °F. Null without either reading.\n","nullable":true,"type":"number"},"code":{"description":"Kalshi reading code (`ok`, `missing`, `late`, `range`, `rate_spatial`, `extreme`, `pending`).","type":"string"},"contributes":{"type":"boolean"},"hour_high_f":{"description":"Highest reading of the station in this minute's settle hour (after hour_end − 60 min) through this minute.","nullable":true,"type":"number"},"hour_low_f":{"description":"Lowest reading of the station in this minute's settle hour through this minute.","nullable":true,"type":"number"},"obs_time":{"description":"Observation time of a METAR fallback reading.","format":"date-time","type":"string"},"offset_c":{"nullable":true,"type":"number"},"primary_code":{"type":"string"},"pull_f":{"description":"Leave-one-out pull on the index at this minute, °F: the index minus the index recomputed\nwithout this station (Kalshi's formula over the contributing members). Null when the\nstation doesn't contribute or is the only contributor.\n","nullable":true,"type":"number"},"received_at":{"description":"Kalshi's receipt time of the reading.","format":"date-time","type":"string"},"source":{"description":"`hf_asos` (1-minute ASOS) or `metar` (fallback).","nullable":true,"type":"string"},"station_id":{"type":"string"},"temp_c":{"nullable":true,"type":"number"},"temp_f":{"nullable":true,"type":"number"},"weight":{"type":"number"}},"type":"object"},"HourlyIndexStrikeDistance":{"description":"Where a value sits against the open event's ladder (N.99 floor strikes): the nearest\nstrike, the signed distance to it and the side. All null for an index-only city.\n","properties":{"distance_to_strike_f":{"description":"The value minus `nearest_strike_f` (°F); positive above it.","example":0.08,"nullable":true,"type":"number"},"nearest_strike_f":{"example":65.99,"nullable":true,"type":"number"},"strike_side":{"description":"`above` when the value is above the strike (the strike's \"greater\" market wins), else `below`.","enum":["above","below",null],"nullable":true,"type":"string"}},"type":"object"},"Market":{"properties":{"cap_strike":{"description":"Inclusive or exclusive upper strike bound depending on strike_type.","nullable":true,"type":"number"},"city":{"type":"string"},"computed_outcome":{"description":"ForecastEx markets only. MinuteTemp's own settlement of the contract from its observations, once computed.","nullable":true,"type":"string"},"condition_id":{"description":"Polymarket condition ID (only present for Polymarket markets)","type":"string"},"event_date":{"description":"Contract date represented by the market.","format":"date","type":"string"},"event_ticker":{"description":"Kalshi event ticker for contract grouping; for ForecastEx, the product ID (e.g. UHLAX).","type":"string"},"event_type":{"enum":["high","low"],"type":"string"},"expires_at":{"description":"ForecastEx markets only. When the contract expires.","format":"date-time","type":"string"},"floor_strike":{"description":"Inclusive or exclusive lower strike bound depending on strike_type.","nullable":true,"type":"number"},"id":{"description":"Market identifier. For Kalshi markets this is the market ticker\n(e.g. HIGHNYC-26FEB11-T33). For Polymarket markets this is the\ninternal UUID. For ForecastEx markets (the contracts Robinhood and\nIBKR broker) this is the contract ID (e.g. UHLAX_101626_100).\nUse this value with GET /api/v1/markets/{market_id}.\n","type":"string"},"no_price":{"deprecated":true,"description":"Deprecated: the public API serves no market prices (since v1.16); always absent.","type":"number"},"platform":{"enum":["kalshi","polymarket","forecastex"],"type":"string"},"settlement_result":{"description":"The side the market settled on, once it settles; absent before.","enum":["yes","no"],"type":"string"},"slug":{"description":"Polymarket market slug (only present for Polymarket markets)","type":"string"},"status":{"description":"ForecastEx markets only. Open until its last trade, closed until ForecastEx settles it.","enum":["open","closed","settled"],"type":"string"},"strike_type":{"description":"Kalshi strike semantics (e.g. between, greater_or_equal). ForecastEx contracts are\nthresholds: `greater` (YES when the temperature exceeds `floor_strike`, a daily high)\nor `less` (YES when it is below `cap_strike`, a daily low).\n","type":"string"},"strike_value":{"type":"number"},"updated_at":{"description":"When the market was last updated (Kalshi and Polymarket). In `GET /api/v1/markets`, when\nMinuteTemp last updated the market's record, not a price time (since v1.17). Absent for\nForecastEx markets.\n","format":"date-time","type":"string"},"volume":{"deprecated":true,"description":"Deprecated: the public API serves no market prices (since v1.16); always absent.","type":"integer"},"yes_price":{"deprecated":true,"description":"Deprecated: the public API serves no market prices (since v1.16); always absent.","type":"number"}},"type":"object"},"MarketHistoryPoint":{"properties":{"computed_outcome":{"description":"ForecastEx markets only. MinuteTemp's own settlement of the contract, once computed.","nullable":true,"type":"string"},"is_settlement":{"description":"True on the settlement point.","type":"boolean"},"no_price":{"deprecated":true,"description":"Deprecated, always null (since v1.16 the public API serves no market prices).","nullable":true,"type":"number"},"settlement_result":{"description":"On a settlement point, the side the market settled on.","enum":["yes","no"],"type":"string"},"timestamp":{"format":"date-time","type":"string"},"volume":{"deprecated":true,"description":"Deprecated, always null (since v1.16 the public API serves no market prices).","nullable":true,"type":"number"},"yes_price":{"deprecated":true,"description":"Deprecated, always null (since v1.16 the public API serves no market prices).","nullable":true,"type":"number"}},"type":"object"},"Observation":{"properties":{"barometric_pressure":{"nullable":true,"type":"number"},"dewpoint":{"description":"Dew point in Celsius","nullable":true,"type":"number"},"heat_index":{"nullable":true,"type":"number"},"is_from_report":{"default":false,"description":"True when this observation originates from an official station report.","type":"boolean"},"is_locf":{"default":false,"description":"Compatibility field retained for older clients. New backend-produced observation payloads set this to false.","type":"boolean"},"observation_time":{"format":"date-time","type":"string"},"precipitation_1h":{"nullable":true,"type":"number"},"precipitation_3h":{"nullable":true,"type":"number"},"precipitation_6h":{"nullable":true,"type":"number"},"preliminary":{"default":false,"description":"True when this is a low-latency observation emitted before the final durable flush.","type":"boolean"},"relative_humidity":{"nullable":true,"type":"number"},"report_type":{"allOf":[{"$ref":"#/components/schemas/ReportType"}],"description":"Report type source when `is_from_report` is true.","nullable":true},"sea_level_pressure":{"nullable":true,"type":"number"},"source_report_id":{"description":"Internal `station_reports.id` for provenance linking.","nullable":true,"type":"string"},"temp_max_c":{"description":"Upper bound of the temperature precision range (Celsius). Derived from integer Celsius measurement precision (+/-0.5C).\n","nullable":true,"type":"number"},"temp_max_f":{"description":"Upper bound of the temperature precision range (Fahrenheit). Derived from integer Celsius measurement precision (+/-0.5C).\n","nullable":true,"type":"number"},"temp_min_c":{"description":"Lower bound of the temperature precision range (Celsius). Derived from integer Celsius measurement precision (+/-0.5C).\n","nullable":true,"type":"number"},"temp_min_f":{"description":"Lower bound of the temperature precision range (Fahrenheit). Derived from integer Celsius measurement precision (+/-0.5C).\n","nullable":true,"type":"number"},"temperature_c":{"description":"Temperature in Celsius","example":22.5,"nullable":true,"type":"number"},"temperature_f":{"description":"Temperature in Fahrenheit","example":72.5,"nullable":true,"type":"number"},"text_description":{"example":"Partly Cloudy","type":"string"},"visibility":{"description":"Visibility in meters. Omitted when unknown.","nullable":true,"type":"number"},"weather_fields_observed_at":{"description":"Set when the non-temperature fields (dewpoint, humidity, pressure, wind, precipitation) were carried forward from an earlier measurement rather than measured at `observation_time`. Holds the observation time of the oldest carried value. Absent when every field was measured at `observation_time`. Stations with 1-minute temperature and slower weather fields (5-minute HFMETAR, or a 30- to 60-minute METAR) carry the latest values until the next update, for at most 75 minutes.\n","format":"date-time","nullable":true,"type":"string"},"wind_chill":{"nullable":true,"type":"number"},"wind_direction":{"nullable":true,"type":"number"},"wind_gust":{"nullable":true,"type":"number"},"wind_speed":{"nullable":true,"type":"number"}},"type":"object"},"OracleErrorDistribution":{"description":"Mergeable signed temperature errors in Fahrenheit (forecast minus observed; positive is warm).\nEach qualifying forecast run contributes one high and one low sample. There are 23 counters:\nunderflow below -10.5°F, 21 zero-centered 1°F bands, and overflow at or above +10.5°F.\nInterior bins include their lower edge and exclude their upper edge. Sum matching counters\nacross days, not daily percentiles. Unlike MAE/bias (equal daily weighting), this is run-weighted.\nNull means unavailable, never zero error. Older scores are not fabricated after forecasts expire.\nCoverage is partial when this day_count is less than the model score's day_count.\n","properties":{"bin_edges_f":{"description":"Finite boundaries -10.5, -9.5, ..., +10.5, in Fahrenheit.","items":{"type":"number"},"maxItems":22,"minItems":22,"type":"array"},"day_count":{"description":"Scored days with histogram coverage (one for a daily row).","minimum":1,"type":"integer"},"high_counts":{"items":{"format":"int64","minimum":0,"type":"integer"},"maxItems":23,"minItems":23,"type":"array"},"low_counts":{"items":{"format":"int64","minimum":0,"type":"integer"},"maxItems":23,"minItems":23,"type":"array"},"sample_count":{"description":"Sum of high_counts, also equal to the sum of low_counts; not their combined sum.","format":"int64","minimum":1,"type":"integer"},"version":{"enum":["signed-error-f-v1"],"type":"string"}},"required":["version","bin_edges_f","high_counts","low_counts","sample_count","day_count"],"type":"object"},"OracleModelScore":{"properties":{"combined_mae":{"example":1.75,"type":"number"},"day_count":{"example":7,"type":"integer"},"error_distribution":{"nullable":true,"oneOf":[{"$ref":"#/components/schemas/OracleErrorDistribution"},{"enum":[null]}],"x-go-type":"OracleErrorDistribution"},"high_bias":{"example":0.8,"type":"number"},"high_mae":{"example":1.4,"type":"number"},"is_public":{"type":"boolean"},"low_bias":{"example":-0.6,"type":"number"},"low_mae":{"example":2.1,"type":"number"},"model_id":{"example":"ncep_hrrr_conus","type":"string"},"model_name":{"example":"HRRR CONUS","type":"string"}},"type":"object"},"OracleRankBy":{"description":"Which MAE metric controls the returned ranking order.","enum":["combined","high","low"],"type":"string"},"OracleScoreData":{"properties":{"all_time":{"description":"Present and true when `days=all` was requested (Clanker tier). Indicates aggregates\nspan from the earliest to latest retained scoring day available for the station.\n","type":"boolean"},"days_requested":{"example":7,"type":"integer"},"range_end":{"format":"date","type":"string"},"range_start":{"format":"date","type":"string"},"rank_by":{"$ref":"#/components/schemas/OracleRankBy"},"score_mode":{"$ref":"#/components/schemas/OracleScoreMode"},"scores":{"items":{"$ref":"#/components/schemas/OracleModelScore"},"type":"array"},"station_id":{"example":"KMDW","type":"string"}},"type":"object"},"OracleScoreDayRow":{"description":"One daily station/model/mode oracle scoring component row. Included rows contain enough\ncomponents to recompute aggregate MAE and bias metrics locally:\n`high_mae = avg(high_abs_error_f)`, `low_mae = avg(low_abs_error_f)`,\n`combined_mae = avg(combined_abs_error_f)`, `high_bias = avg(high_error_f)`,\n`low_bias = avg(low_error_f)`, and `day_count = count(included rows)`.\nIn `oracle-v1`, `combined_abs_error_f = (high_abs_error_f + low_abs_error_f) / 2`.\n`forecast_high_f` and `forecast_low_f` are daily mean forecast extrema across contributing\nruns when observed high/low values are available; `forecast_run_id` is null for multi-run\ndaily averages.\n","properties":{"combined_abs_error_f":{"description":"Formula version `oracle-v1`: `(high_abs_error_f + low_abs_error_f) / 2`.\n","example":1.3,"nullable":true,"type":"number"},"error_distribution":{"nullable":true,"oneOf":[{"$ref":"#/components/schemas/OracleErrorDistribution"},{"enum":[null]}],"x-go-type":"OracleErrorDistribution"},"exclusion_reason":{"description":"Reason the row is excluded when `included=false`.","example":"missing_model_score","nullable":true,"type":"string"},"forecast_high_f":{"example":91.2,"nullable":true,"type":"number"},"forecast_issued_at":{"description":"Present only when a daily row maps to a single contributing forecast run.","format":"date-time","nullable":true,"type":"string"},"forecast_low_f":{"example":72.4,"nullable":true,"type":"number"},"forecast_run_id":{"description":"Present only when a daily row maps to a single contributing forecast run.","nullable":true,"type":"string"},"high_abs_error_f":{"example":1.2,"nullable":true,"type":"number"},"high_error_f":{"description":"Signed daily high error in Fahrenheit (`forecast_high_f - observed_high_f`).","example":1.2,"nullable":true,"type":"number"},"included":{"description":"Whether this row contributes to aggregate oracle scoring.","type":"boolean"},"is_public":{"type":"boolean"},"low_abs_error_f":{"example":1.4,"nullable":true,"type":"number"},"low_error_f":{"description":"Signed daily low error in Fahrenheit (`forecast_low_f - observed_low_f`).","example":1.4,"nullable":true,"type":"number"},"model_id":{"example":"ncep_hrrr_conus","type":"string"},"model_name":{"example":"HRRR CONUS","type":"string"},"observation_source":{"example":"asos","nullable":true,"type":"string"},"observed_high_f":{"example":90,"nullable":true,"type":"number"},"observed_low_f":{"example":71,"nullable":true,"type":"number"},"score_mode":{"$ref":"#/components/schemas/OracleScoreMode"},"valid_date":{"example":"2026-06-23","format":"date","type":"string"}},"type":"object"},"OracleScoreDaysData":{"properties":{"all_time":{"description":"Present and true when `days=all` was requested by a Clanker key.","type":"boolean"},"days_requested":{"description":"Effective requested scored-day window after plan capping.","example":30,"type":"integer"},"formula_version":{"enum":["oracle-v1"],"type":"string"},"max_history_days":{"description":"Maximum numeric `days` or explicit date span allowed by the authenticated plan.","example":30,"type":"integer"},"modes":{"items":{"$ref":"#/components/schemas/OracleScoreMode"},"type":"array"},"range_end":{"format":"date","type":"string"},"range_start":{"format":"date","type":"string"},"rows":{"items":{"$ref":"#/components/schemas/OracleScoreDayRow"},"type":"array"},"station_id":{"example":"KMDW","type":"string"}},"type":"object"},"OracleScoreMode":{"description":"`overall` evaluates all forecast runs that targeted the scored local day.\n`day_ahead` evaluates only runs fetched during the previous local day.\n`day_of` evaluates only runs fetched during the scored local day.\n","enum":["overall","day_ahead","day_of"],"type":"string"},"ReportClockSchedule":{"description":"Clock-based report schedule definition.","properties":{"basis":{"description":"Clock basis for `hour` and `minute`.","enum":["utc","local"],"type":"string"},"hour":{"maximum":23,"minimum":0,"type":"integer"},"label":{"type":"string"},"local_hour":{"maximum":23,"minimum":0,"type":"integer"},"local_minute":{"maximum":59,"minimum":0,"type":"integer"},"minute":{"maximum":59,"minimum":0,"type":"integer"},"utc_hour":{"maximum":23,"minimum":0,"type":"integer"},"utc_minute":{"maximum":59,"minimum":0,"type":"integer"}},"type":"object"},"ReportIntervalSchedule":{"description":"Interval-based report schedule definition.","properties":{"basis":{"description":"Clock basis for `minute`.","enum":["utc","local"],"type":"string"},"interval_minutes":{"minimum":1,"type":"integer"},"label":{"type":"string"},"local_minute":{"maximum":59,"minimum":0,"type":"integer"},"minute":{"maximum":59,"minimum":0,"type":"integer"},"utc_minute":{"maximum":59,"minimum":0,"type":"integer"}},"required":["interval_minutes"],"type":"object"},"ReportMultiHourSchedule":{"description":"Multi-hour schedule definition for repeated daily clock slots.","properties":{"label":{"type":"string"},"local_hours":{"items":{"maximum":23,"minimum":0,"type":"integer"},"type":"array"},"local_minute":{"maximum":59,"minimum":0,"type":"integer"},"utc_hours":{"items":{"maximum":23,"minimum":0,"type":"integer"},"type":"array"},"utc_minute":{"maximum":59,"minimum":0,"type":"integer"}},"type":"object"},"ReportSchedules":{"additionalProperties":{"anyOf":[{"$ref":"#/components/schemas/ReportClockSchedule"},{"$ref":"#/components/schemas/ReportIntervalSchedule"},{"$ref":"#/components/schemas/ReportMultiHourSchedule"},{"items":{"anyOf":[{"$ref":"#/components/schemas/ReportClockSchedule"},{"$ref":"#/components/schemas/ReportIntervalSchedule"},{"$ref":"#/components/schemas/ReportMultiHourSchedule"}]},"type":"array"}]},"description":"Per-report-type expected MinuteTemp UI arrival schedules from station configuration,\nincluding normal source and ingestion delay. Values can be a single schedule object or\nan array of schedules.\n","type":"object"},"ReportType":{"description":"Official station report type.","enum":["cli","dsm","metar_tgroup","metar_6hr"],"type":"string"},"Station":{"properties":{"ibkr_active":{"type":"boolean"},"id":{"format":"uuid","type":"string"},"kalshi_active":{"type":"boolean"},"latitude":{"example":41.7861,"type":"number"},"longitude":{"example":-87.7522,"type":"number"},"name":{"example":"Chicago Midway Airport","type":"string"},"polymarket_active":{"description":"Settlement station of a Polymarket (polymarket.com) temperature market.","type":"boolean"},"polymarket_us_active":{"description":"Settlement station of a Polymarket US (CFTC-regulated US exchange) temperature market. These settle on the NWS Daily Climate Report (CLI).","type":"boolean"},"robinhood_active":{"type":"boolean"},"station_id":{"description":"ICAO code","example":"KMDW","type":"string"},"wu_enabled":{"description":"True when Weather Underground location mapping is configured for this station.","type":"boolean"}},"type":"object"},"StationFlat":{"description":"Station with parent city context","properties":{"city_name":{"example":"Chicago","type":"string"},"city_slug":{"example":"chi","type":"string"},"ibkr_active":{"type":"boolean"},"id":{"format":"uuid","type":"string"},"kalshi_active":{"type":"boolean"},"latitude":{"example":41.7861,"type":"number"},"longitude":{"example":-87.7522,"type":"number"},"name":{"example":"Chicago Midway Airport","type":"string"},"polymarket_active":{"description":"Settlement station of a Polymarket (polymarket.com) temperature market.","type":"boolean"},"polymarket_us_active":{"description":"Settlement station of a Polymarket US (CFTC-regulated US exchange) temperature market. These settle on the NWS Daily Climate Report (CLI).","type":"boolean"},"robinhood_active":{"type":"boolean"},"station_id":{"description":"ICAO code","example":"KMDW","type":"string"},"wu_enabled":{"description":"True when Weather Underground location mapping is configured for this station.","type":"boolean"}},"type":"object"},"StationReport":{"properties":{"fetched_at":{"description":"Time MinuteTemp fetched this report.","format":"date-time","type":"string"},"issuance_time":{"description":"Time the source system issued this report.","format":"date-time","type":"string"},"live_published_at":{"description":"Time MinuteTemp handed this report to Redis Pub/Sub when available.","format":"date-time","nullable":true,"type":"string"},"max_temp_c":{"nullable":true,"type":"number"},"max_temp_f":{"nullable":true,"type":"number"},"max_temp_time_utc":{"description":"UTC timestamp when the report says the daily high occurred.","format":"date-time","nullable":true,"type":"string"},"min_temp_c":{"nullable":true,"type":"number"},"min_temp_f":{"nullable":true,"type":"number"},"min_temp_time_utc":{"description":"UTC timestamp when the report says the daily low occurred.","format":"date-time","nullable":true,"type":"string"},"provider":{"description":"Upstream provider identifier.","example":"nwscli","type":"string"},"report_date":{"description":"Target local calendar date represented by the report.","example":"2026-02-27T00:00:00Z","format":"date","type":"string"},"report_id":{"description":"Internal report row identifier.","type":"string"},"report_revision":{"description":"Monotonic revision for report corrections.","type":"integer"},"report_type":{"$ref":"#/components/schemas/ReportType"},"report_updated_at":{"description":"Last update timestamp for this report row.","format":"date-time","type":"string"},"source_url":{"description":"Source URL fetched to produce the report.","nullable":true,"type":"string"},"temp_c":{"description":"Point temperature for `metar_tgroup` reports.","nullable":true,"type":"number"},"temp_f":{"description":"Point temperature for `metar_tgroup` reports.","nullable":true,"type":"number"},"wmo_emit_time":{"description":"WMO text-product emit time when available; distinct from report observation/issuance time.","format":"date-time","nullable":true,"type":"string"}},"type":"object"},"StationWeatherEvents":{"properties":{"active":{"items":{"$ref":"#/components/schemas/WeatherEvent"},"type":"array"},"city":{"$ref":"#/components/schemas/City"},"generated_at":{"format":"date-time","type":"string"},"recent_ended":{"items":{"$ref":"#/components/schemas/WeatherEvent"},"type":"array"},"station":{"$ref":"#/components/schemas/Station"}},"type":"object"},"WeatherEvent":{"properties":{"badge":{"type":"string"},"detail":{"type":"string"},"ended_at":{"format":"date-time","nullable":true,"type":"string"},"event_type":{"example":"thunderstorm","type":"string"},"id":{"format":"uuid","type":"string"},"last_confirmed_at":{"format":"date-time","type":"string"},"name":{"type":"string"},"source":{"$ref":"#/components/schemas/WeatherEventSource"},"started_at":{"format":"date-time","type":"string"},"state":{"enum":["active","ended"],"type":"string"},"summary":{"type":"string"},"tier":{"enum":["tier1","tier2","tier3"],"type":"string"}},"type":"object"},"WeatherEventSource":{"properties":{"cb_location":{"type":"string"},"flight_category":{"type":"string"},"metar_type":{"type":"string"},"peak_wind_direction":{"nullable":true,"type":"integer"},"peak_wind_kt":{"nullable":true,"type":"number"},"visibility_mi":{"nullable":true,"type":"number"},"wind_gust_kt":{"nullable":true,"type":"number"},"wind_speed_kt":{"nullable":true,"type":"number"},"wx_string":{"type":"string"},"wx_token":{"type":"string"}},"type":"object"},"WsTicket":{"properties":{"ticket":{"description":"JWT token valid for 90 seconds","type":"string"}},"type":"object"}},"securitySchemes":{"apiKeyHeader":{"description":"API key passed in X-API-Key header","in":"header","name":"X-API-Key","type":"apiKey"},"bearerAuth":{"description":"API key passed as Bearer token","scheme":"bearer","type":"http"}}},"info":{"description":"Real-time, minute-level weather data for MinuteTemp stations.\n\nThis OpenAPI document covers the public, API-key-authenticated contract under `/api/v1/*`.\nMinuteTemp also has first-party frontend endpoints under `/ui/v1/*`, but those are internal\nweb-app routes used by the product UI and are not part of the stable external API contract.\n\n## Getting Started\n1. Sign up at [minutetemp.com](https://minutetemp.com) and generate an API key from your profile.\n2. Include your key in every request via `X-API-Key` header or `Authorization: Bearer \u003ckey\u003e`.\n3. Browse endpoints below or try them interactively.\n\n```bash\ncurl -H \"X-API-Key: your_api_key\" https://api.minutetemp.com/api/v1/stations\n```\n\n## WebSocket (Real-Time Push)\nPaid API WebSocket streams deliver **1-minute** observation events in realtime. See the\n[WebSocket docs](/docs/ws/) for connection details, payload shapes, and the separate product UI\nstream (`/ws/ui`) where free users receive the same 1-minute data on a 3-minute delay.\n\n## Bot Startup Recipe\n1. Fetch baseline snapshots via REST (`/api/v1/stations/{station_id}/forecast/runs` and `/api/v1/markets/{market_id}/history`).\n2. Open WebSocket and send `subscribe` for live invalidations.\n3. Persist `sequence` from WS envelopes.\n4. On disconnect or detected gap, catch up via cursor endpoints, then resume streaming.\n\n## Changelog\n- v1.17 behavior change:\n  - In `GET /api/v1/markets`, the `updated_at` of a Kalshi or Polymarket market is when\n    MinuteTemp last updated the market's record (its metadata), not when its prices were last\n    polled, which every market of a platform shared. The list also includes open markets\n    that have no price yet. The fields served are otherwise unchanged.\n- v1.16 behavior change:\n  - The public API no longer serves market prices, for any platform (Kalshi, Polymarket,\n    ForecastEx). Markets in `GET /api/v1/markets` and `GET /api/v1/markets/{market_id}` omit\n    `yes_price`, `no_price` and `volume`; a ForecastEx market also omits `updated_at`. Market\n    history points keep their timestamps with `yes_price`, `no_price` and `volume` null. The\n    `ladder` of `GET /api/v1/hourly-indexes/{city}` keeps its strikes with every quote\n    (`yes_bid`, `yes_ask`, `no_bid`, `no_ask`, `last_price`, `volume`) null. These fields\n    stay in the schema, deprecated. Markets, strikes, statuses, settlement points and settled\n    temperatures are served as before.\n  - Markets and settlement points add `settlement_result` (`yes` or `no`) once a market\n    settles, in place of the settled prices.\n- v1.15 behavior change:\n  - ForecastEx price display is now enabled by default, so ForecastEx markets carry\n    `yes_price` and `no_price` (the last traded prices, not a bid or ask), `volume` and\n    `updated_at`, and a ForecastEx settlement point in the history carries `yes_price`\n    (1 or 0) and `no_price`. MinuteTemp can still turn price display off, and these fields\n    are then absent again.\n- v1.14 additive:\n  - Markets add `platform: forecastex`: the daily high and low contracts of ForecastEx, which\n    Robinhood and IBKR broker, in `GET /api/v1/markets` (and `platform=forecastex`),\n    `GET /api/v1/markets/{market_id}` and its history (the settlement point only). Their\n    `expires_at`, `status` and `computed_outcome` are always present; `yes_price`,\n    `no_price`, `volume` and `updated_at` (when the prices were seen) only while ForecastEx\n    price display is enabled.\n- v1.13 behavior change:\n  - `GET /api/v1/markets/{market_id}/history` for a Polymarket market returns only the\n    settlement point, when the market has resolved inside the requested range, and an empty\n    `points` list otherwise. Polymarket price history is no longer stored. Kalshi markets are\n    unchanged.\n- v1.12 additive:\n  - `GET /api/v1/limits` adds `ws_subscriptions` with the caller's plan's WebSocket market and\n    neighbour subscription limits (`null` means unlimited).\n- v1.11 additive:\n  - External WebSocket: observation-only (neighbour) stations have their own subscription\n    quota (Starter 30, Pro 300, Clanker unlimited) beside the unchanged market quota; see\n    **Plan Limits** and the [WebSocket docs](/docs/ws/).\n- v1.10 additive:\n  - Hourly index data files (see **Hourly index data files**): each index city's forecast,\n    both models, as a current file and one day file per UTC date. `GET /api/v1/data/access`\n    adds `hourly_indexes`, `hourly_index_days` and `public.hourly_indexes_prefix`, and the\n    WebSocket adds `index_file_updated` and the snapshot's `file` (see the\n    [WebSocket docs](/docs/ws/)).\n- v1.10 deprecation:\n  - `GET /api/v1/hourly-indexes/{city}/forecast` and\n    `GET /api/v1/hourly-indexes/{city}/forecast/history` are deprecated: read the hourly index\n    data files instead. Both keep working with unchanged responses.\n- v1.9 behavior change:\n  - Forecast runs follow the plan's history window (see **Plan Limits**). A run is readable\n    while the UTC date it was fetched is today or one of the `max_history_days` dates before\n    it. `GET /api/v1/stations/{station_id}/forecast` leaves out a model whose newest run is\n    older and answers `403` when no model is left;\n    `GET /api/v1/stations/{station_id}/forecast/runs/{run_id}` answers `403` for an older run;\n    `GET /api/v1/stations/{station_id}/forecast/runs` returns runs from the start of the window\n    and answers `403` when the whole range is older.\n- v1.9 deprecation:\n  - `GET /api/v1/stations/{station_id}/forecast` and\n    `GET /api/v1/stations/{station_id}/forecast/runs/{run_id}` are deprecated: read forecasts\n    as files (see **Getting forecast data**). Both keep working with unchanged responses.\n- v1.9 additive:\n  - Forecast data files (see **Forecast data files**): forecast run summaries from\n    `GET /api/v1/stations/{station_id}/forecast/runs` and\n    `GET /api/v1/stations/{station_id}/forecast/runs/{run_id}` add `object_path`, the path of\n    the run's file, for runs that have one; the field is absent for the others. Every other\n    field of those responses is unchanged.\n  - RRFS next to HRRR in the hourly index forecast. `GET /api/v1/hourly-indexes/{city}/forecast`,\n    `/forecast/history` and `/forecast-accuracy` take an optional `model`\n    (`ncep_hrrr_conus_15min`, the default, or `ncep_rrfs_conus_15min`; anything else is 400) and\n    return `model_id` (new on `/forecast` and `/forecast-accuracy`). Each model is computed from\n    its own member runs with the same formula, calibration and quorum, and has its own bias,\n    history and accuracy; one is never filled in from the other. A model without a forecast for\n    a city answers `forecast: null` (RRFS is published only where the collector has it enabled\n    and the members have RRFS runs; an RRFS forecast with no step left, or whose members lost\n    their RRFS forecasts, is removed rather than served). Without `model`, every response is\n    what it was.\n  - The city body (and the WebSocket snapshot) adds `forecasts`: the other models' forecasts by\n    model ID, next to the unchanged HRRR `forecast`. The key is absent while there are none.\n  - Hourly index minutes add `restated_f` and `restated_at`: a Kalshi restatement of an official\n    value no longer replaces `official_f`/`official_at` (the first official value, which the\n    settlement, the forecast history's `adjusted_f` and the forecast accuracy use).\n  - Hourly index settlements add the status `incomplete_data` (our minutes have a gap after the\n    last official minute; the gap is in `missing_minutes`), and `settle_source` (`computed` or\n    `kalshi_expiration`). A market city's `incomplete_data` hour takes Kalshi's final value at\n    reconciliation, with `match` null, and is left out of the match rate.\n  - Forecast history `adjusted_f` takes its bias only from minutes already official at each\n    run's fetch, so it never uses a value known later.\n  - The hourly index `current_hour` carries `feed_conditions`: degradations of the city's\n    index feed over its last 30 final minutes (a member on its METAR fallback, excluded, or on\n    a QC substitute; quorum at risk; unavailable minutes). The city body adds\n    `contract_terms_url`, the Kalshi contract terms of the city's hourly series.\n  - Added `/api/v1/hourly-indexes/{city}/forecast-accuracy?window=7d|30d`: MAE, mean signed\n    error and strike match share of the bias-adjusted and raw HRRR index forecast at 60, 30 and\n    15 minutes before each determined hour's close.\n  - The hourly index `current_hour` places values against the ladder: `settles_now_as` adds\n    `implied_floor_strike`, `nearest_strike_f`, `distance_to_strike_f` and `strike_side`, and\n    the adjusted forecast settle adds `forecast_adjusted_nearest_strike_f`,\n    `forecast_adjusted_distance_to_strike_f` and `forecast_adjusted_strike_side` (null for\n    index-only cities).\n  - The hourly index forecast (`GET /api/v1/hourly-indexes/{city}/forecast`, the city body's\n    `forecast`) carries the bias-adjusted forecast: `bias_f`, `bias_as_of_minute`,\n    `adjusted_f` and `member_bias`, and each `settles[]` entry `adjusted_value_f` and\n    `adjusted_floor_strike`.\n  - Hourly index data is never delayed: the UI's free delayed hourly view is gone (in the UI,\n    market cities need a paid plan, UI and above with trials included, and index-only cities\n    are open in realtime). The API was already realtime and still requires Starter, so API\n    responses are unchanged.\n  - Removed `trend_settle_f` and `trend_floor_strike` from the hourly index `current_hour`\n    (the 15-minute trend line); use `forecast_adjusted_settle_f` for where the hour is heading.\n  - Member readings in hourly index minutes (`include=stations`, the city snapshot's minutes)\n    carry per-station metrics: `pull_f` (leave-one-out pull on the index), `change_5m_f`,\n    `hour_high_f` and `hour_low_f`.\n- v1.8 additive:\n  - Added `/api/v1/hourly-indexes/{city}/forecast/history`: the stitched HRRR index forecast\n    for a window (up to 48 hours), each member's 15-minute temperature taken from its newest\n    run fetched at or before the step, with per-member series and the as-forecast\n    bias-adjusted series `adjusted_f`.\n  - Added the bias-adjusted HRRR forecast settle to the hourly index `current_hour`:\n    `forecast_settle_f`, `forecast_bias_f`, `forecast_adjusted_settle_f` and\n    `forecast_adjusted_floor_strike` (null without a forecast covering the hour, a valued\n    minute, or quorum).\n- v1.7 additive:\n  - Added `/api/v1/hourly-indexes/{city}/forecast`: the city's index forecast from its member\n    stations' HRRR 15-minute temperatures (Kalshi's formula and current calibration), with\n    per-member series, the forecast settle of the next top-of-hours and the implied strike.\n    The same object is the `forecast` field of the city body and the WebSocket\n    `forecast_index` event.\n- v1.6 additive:\n  - Added **Hourly Indexes**: `/api/v1/hourly-indexes` (city list), `/{city}` (current state,\n    the WebSocket snapshot body), `/{city}/minutes`, `/{city}/settlements`,\n    `/{city}/calibrations` and `/{city}/match-rate`. Live updates use the WebSocket\n    `hourly_indexes` subscription (1 subscription slot per city) with a per-city `seq` and\n    `resume`; see the [WebSocket docs](/docs/ws/).\n- v1.5 additive:\n  - Added **Plan Limits** section documenting per-subscription caps for history, oracle scores,\n    observations, reports, forecast models, WebSocket subscriptions, and rate limits.\n  - Clarified paid REST observation `resolution` is always `1m`; external WebSocket uses realtime\n    1-minute events (`/ws/api/5m` is a legacy path with the same cadence).\n  - Added `/api/v1/stations/{station_id}/oracle-score-days` for daily oracle score components\n    that can be locally aggregated into arbitrary rolling windows.\n- v1.4 additive:\n  - Added optional `day_mode` on latest station observations.\n  - Added `temperature_day_mode`, `temperature_day_date`, `wu_day_mode`, and `wu_day_date` on latest observation payloads.\n- v1.3 additive:\n  - Added station weather events endpoint for active and recently ended METAR/SPECI-driven event state.\n- v1.2 additive:\n  - Added station report endpoints for official CLI/DSM/METAR reports.\n  - Added report provenance fields on observation payloads.\n  - Added 5-minute observation resolution enum for starter tier.\n- v1.1 additive:\n- Added market history endpoint with cursor pagination.\n- Added forecast run list/detail endpoints for deterministic replay.\n- Added limits introspection endpoint.\n- Added `is_locf` on observation payloads.\n\n## Rate Limits\n\nPer-subscription request caps are listed under **Plan Limits** below. Limits are shared across\nall API keys for the same user account.\n\nAdditional IP safeguard on `/api/v1/*` (before API key auth):\n- 20 requests/second sustained\n- burst up to 100 requests\n\nEffective limit is whichever guard is hit first (IP or per-user tier).\nPer-user plan limits use a token bucket that refills at the plan's requests/minute rate and stores up to one minute of tokens while idle. The listed burst value is the maximum a fully idle client can spend across the next minute.\n\n`GET /api/v1/limits` returns a region-scoped snapshot for the request that served it.\n\nRate limit headers:\n- `X-RateLimit-Remaining` — immediate stored tokens remaining after the request\n- `X-RateLimit-Reset` — seconds until the immediate token bucket fully refills\n- `Retry-After` — seconds to wait (only on 429)\n\n## Plan Limits\n\nThe external API requires a **Starter, Pro, or Clanker** subscription and a valid API key.\nFree and UI-only accounts receive `403` on `/api/v1/*` routes.\n\n`GET /api/v1/limits` returns the authenticated key's effective values, including live\nrate-limit snapshots. Static caps by subscription:\n\n### Rate limits\n\n| Plan    | Requests/min | Burst after idle |\n|---------|--------------|------------------|\n| Starter | 15           | 30               |\n| Pro     | 100          | 200              |\n| Clanker | 250          | 500              |\n\nLimits are shared across all API keys for the same user account.\n\n### Historical data window (`max_history_days`)\n\nMaximum inclusive calendar-day span for time-bounded history endpoints: market snapshot\nhistory, forecast run lists, report history date ranges, and oracle score lookback.\n\n| Plan    | Max history days |\n|---------|------------------|\n| Starter | 7                |\n| Pro     | 30               |\n| Clanker | 90               |\n\nForecast runs are also limited by age: a run is readable while the UTC date of its\n`fetched_at` is today (UTC) or one of the `max_history_days` UTC dates before it. This applies\nto the latest forecast, the run list and a run fetched by ID. Requests for older runs only\nreceive `403`.\n\n### Oracle scores\n\n| Plan    | Max numeric `days` | `days=all` (full retained history) |\n|---------|--------------------|------------------------------------|\n| Starter | 7                  | No                                 |\n| Pro     | 30                 | No                                 |\n| Clanker | 90                 | Yes                                |\n\nWhen `days` is omitted, the default lookback is 7 scored local days. Numeric values are\nclamped to the plan maximum. Use `/api/v1/stations/{station_id}/oracle-score-days` to fetch\ndaily score components once and derive exact local rolling windows from included rows.\n\n### REST observations\n\nHistorical `GET /api/v1/stations/{station_id}/observations` returns **1-minute** points for all\npaid API plans. The response field `resolution` is always `\"1m\"`.\n\n| Plan    | Resolution | Max `start`–`end` span per request |\n|---------|------------|-----------------------------------|\n| Starter | 1 minute   | 7 days                            |\n| Pro     | 1 minute   | 7 days                            |\n| Clanker | 1 minute   | 7 days                            |\n\n### Official reports\n\nAll paid API subscriptions (Starter, Pro, Clanker) may call `/api/v1/stations/{station_id}/reports*`.\nReport history date ranges use the same `max_history_days` window as other history endpoints.\n\n### Forecast models\n\nPaid API subscribers receive every forecast model available at a station on forecast endpoints.\n\n`GET /api/v1/forecasts/models` exposes `is_public` for the three models also available on the\nfree web UI without a paid subscription: `nws_gridpoint`, `ncep_nbm_conus`, and `gfs_global`.\nCalling forecast endpoints through the external API still requires a paid plan.\n\n### External WebSocket (real-time push)\n\nPaid API keys receive **realtime 1-minute** observation events. `/ws/api/1m` is the canonical\nstream; `/ws/api/5m` is a legacy compatibility path with the **same** unthrottled 1-minute\ncadence (the name is not a 5-minute aggregation tier).\n\n| Plan    | Endpoints                    | Max market subscriptions (all connections) | Max neighbour subscriptions (all connections) |\n|---------|------------------------------|--------------------------------------------|-----------------------------------------------|\n| Starter | `/ws/api/1m`, `/ws/api/5m`   | 6                                          | 30                                            |\n| Pro     | `/ws/api/1m`, `/ws/api/5m`   | 60                                         | 300                                           |\n| Clanker | `/ws/api/1m`, `/ws/api/5m`   | 200                                        | Unlimited                                     |\n\nMarket subscriptions are cities, market stations and hourly indexes. Neighbour subscriptions\nare observation-only stations (weather-map and nowcast neighbours, not listed by\n`GET /api/v1/stations`); they do not use market slots. Stations are subscribed one code at a\ntime and the server picks the quota for each code. Within the `stations` list, the newly\nrequested market stations are accepted or rejected together, and so are the newly requested\nneighbours; `cities`, `stations` and `hourly_indexes` are checked in turn, so one field can be\naccepted and a later one rejected. A full quota rejects with a reason naming it and its\nlimit, e.g. `neighbour subscription limit exceeded (limit 30)`.\n\nEach hourly index city subscribed with `hourly_indexes` costs 1 subscription (member\nobservations and the forecast station's forecast and oracle events come with it). Poll the\nhourly index REST endpoints only for cold starts and history; a `429` there means the\nWebSocket is the live path.\n\nObtain a ticket via `POST /api/v1/ws-ticket`. Payload shapes and limits are in\n[WebSocket docs](/docs/ws/). Product UI streaming (`/ws/ui`, including the free-tier 3-minute\ndelivery delay) is documented there but is not part of the external API contract.\n\n## Forecast data files\n\nForecasts are read as files. Every forecast run is published as one JSON file on the data host\n`https://data.minutetemp.com`, served through a CDN. A file holds the whole run: its metadata\nand every forecast step with every weather field collected. A file never changes once it is\nwritten, and file requests do not count against the API rate limits.\n\n`GET /api/v1/stations/{station_id}/forecast` and\n`GET /api/v1/stations/{station_id}/forecast/runs/{run_id}` are deprecated in favour of the\nfiles. They keep working with unchanged responses. `GET /api/v1/stations/{station_id}/forecast/runs`\nis not deprecated: it is how you find the files of past runs.\n\n### Getting forecast data\n\nThree pieces fit together:\n\n- The WebSocket event `forecast_updated` says a new run exists and carries `path`, the path of\n  its file.\n- `GET /api/v1/data/access` returns `base_url` and one token (`query`) per UTC date your plan\n  may read.\n- A file's URL is `base_url` + `path` + `?` + the `query` of the grant for the path's date.\n\n**1. Follow live forecasts over the WebSocket.** Connect to `/ws/api/1m` and subscribe to\ncities or stations as described in the [WebSocket docs](/docs/ws/). Each new run arrives as:\n\n```json\n{\"type\": \"forecast_updated\", \"slug\": \"chi\", \"station_id\": \"KMDW\", \"model_id\": \"ncep_hrrr_conus\",\n \"version\": \"2026-10-12T17:04:11Z\", \"run_id\": \"0199d2a0-0000-7000-8000-000000000001\",\n \"path\": \"/private/v1/forecasts/2026/10/12/KMDW/ncep_hrrr_conus/0199d2a0-0000-7000-8000-000000000001.json\"}\n```\n\nGet the tokens once and keep them while they are valid:\n\n```bash\ncurl -s -H \"X-API-Key: $MINUTETEMP_API_KEY\" https://api.minutetemp.com/api/v1/data/access\n```\n\n```json\n{ \"data\": {\n    \"base_url\": \"https://data.minutetemp.com\",\n    \"expires_at\": \"2026-10-12T18:00:00Z\",\n    \"forecasts\": [\n      { \"date\": \"2026-10-12\", \"prefix\": \"/private/v1/forecasts/2026/10/12/\", \"query\": \"1760288400-c2lnbmF0dXJl\" },\n      { \"date\": \"2026-10-13\", \"prefix\": \"/private/v1/forecasts/2026/10/13/\", \"query\": \"1760288400-b3RoZXJzaWc\" } ],\n    \"public\": { \"chart_prefix\": \"/public/v1/chart/\", \"forecasts_prefix\": \"/public/v1/forecasts/\" } } }\n```\n\n(The `forecasts` list is shortened here; it has one entry per readable date.) Then fetch the\nfile named by the event:\n\n```bash\ncurl -s \"https://data.minutetemp.com/private/v1/forecasts/2026/10/12/KMDW/ncep_hrrr_conus/0199d2a0-0000-7000-8000-000000000001.json?1760288400-c2lnbmF0dXJl\"\n```\n\nIn code:\n\n```text\naccess = GET /api/v1/data/access\n\non WebSocket message m with m.type == \"forecast_updated\":\n    if m.path is absent: skip           # no file advertised for this run\n    run = fetch_file(m.path)\n\nfetch_file(path):\n    if now \u003e access.expires_at - 2 minutes: access = GET /api/v1/data/access\n    grant = the entry of access.forecasts whose prefix path starts with\n    if no grant:\n        access = GET /api/v1/data/access; look again\n        if still no grant: return OUTSIDE_PLAN    # the date is not in your plan's window\n    response = GET access.base_url + path + \"?\" + grant.query\n    if response is 429: wait until the block ends (60 seconds), then GET again\n    if response is 403:\n        access = GET /api/v1/data/access; take the date's grant again; GET once more\n        if still 403: return DENIED              # token refused on a granted date: report it\n    if response is 404: return GONE              # expired (oldest dates) or never published\n    if response is not 200: return FAILED        # retry later\n    return parse_json(response)                  # only a 200 body is a forecast file\n```\n\nA path is sent once, with the event of a newly stored run. After a reconnect, list the runs\nyou missed (step 2) instead of waiting for events.\n\n**2. Pull history for your plan window.** List runs with\n`GET /api/v1/stations/{station_id}/forecast/runs` and fetch each run's `object_path` the same\nway.\n\nTwo limits apply, and they differ. The dates you may read run from UTC midnight\n`max_history_days` days before today through now (the table below), which is longer than\n`max_history_days` days. One request's `start`–`end` span may be at most `max_history_days`\ndays, and `end` defaults to now, so a request from the start of the window without an `end`\nis `400`. Ask for the window in bounded chunks, for example one UTC day per request, with\n`start` and `end` both set, and page each chunk with `cursor`:\n\n```bash\ncurl -s -H \"X-API-Key: $MINUTETEMP_API_KEY\" \\\n  \"https://api.minutetemp.com/api/v1/stations/KMDW/forecast/runs?model_id=ncep_hrrr_conus\u0026start=2026-10-05T00:00:00Z\u0026end=2026-10-05T23:59:59.999Z\"\n```\n\n```text\nfor each UTC date D from the first date of access.forecasts through today:\n    page = GET .../forecast/runs?start=D T00:00:00Z\u0026end=D T23:59:59.999Z\n    repeat until page.next_cursor is null:\n        for each run in page:\n            if run.object_path is absent: skip     # no file for this run\n            result = fetch_file(run.object_path)\n            if result is a forecast file: save(result)\n        page = GET the same URL with cursor=page.next_cursor\n```\n\nA file's date (`YYYY/MM/DD` in its path) is the UTC date the run was fetched (`fetched_at`).\n`forecasts` has one grant per UTC fetch date inside the plan's history window (the same\nwindow the forecast run endpoints apply; see **Plan Limits**), through tomorrow (UTC), so a\nresponse taken before midnight still covers runs fetched after it:\n\n| Plan    | UTC fetch dates readable                |\n|---------|-----------------------------------------|\n| Starter | today and the 7 dates before it         |\n| Pro     | today and the 30 dates before it        |\n| Clanker | today and the 90 dates before it        |\n\nPrivate files are deleted 90 days after they are written, so a file on the oldest dates of a\n90-day window may already be gone (`404`). Downloading a long history runs into the data\nhost's request limit (step 3): keep under 100 requests a minute from one IP address.\n\n**3. Refresh tokens and handle errors.**\n\n- A token is valid for one UTC date, shared by every file of that date, and stops working at\n  `expires_at` (at most one hour after it was issued). The access response may be reused for\n  up to five minutes (`Cache-Control: private, max-age=N`, never past the next UTC midnight);\n  do not share it between API keys.\n- Fetch a new access response shortly before `expires_at`, when the date you need is not\n  listed (a new date appears at UTC midnight), and after a file request answers `403`.\n- `query` is opaque and must be the whole query string: no parameter name and no other\n  parameters.\n- Whether your plan may read a date is decided by the access response: a date outside the\n  plan's window has no entry in `forecasts`. The data host only checks that a token is a\n  valid, unexpired signature for the file's date.\n- `403`: the request under `/private/` had no valid token for that file's date (missing,\n  expired, or for another date). The body is an HTML page from the CDN, not JSON. Refresh\n  the access response and retry once with the date's new grant. A `403` that persists for a\n  date the access response grants is an authorization failure, not a plan limit: stop\n  retrying and report it.\n- `404`: no file at that path: on the oldest dates of a 90-day window the file has expired\n  (private files are kept 90 days), otherwise the path was never published. The body is not\n  JSON. Do not retry.\n- `429`: the data host allows 100 requests per 60 seconds per IP address. Over that, requests\n  from the address are blocked for 60 seconds with `429`; wait, then continue more slowly.\n- `GET /api/v1/data/access` answers `404` (`DATA_ACCESS_NOT_ENABLED`) if file delivery is\n  switched off. Until it answers `200` again, the deprecated forecast endpoints still serve\n  the runs.\n\n**4. Chart files.** The access response also carries a `chart` grant and\n`public.chart_prefix`. They are for the per-station chart file of the MinuteTemp web app. Its\nformat is not part of this API and may change, and the API WebSocket has no event for it.\nUse the run files.\n\n**5. Free public files.** A run of one of the three free models (`nws_gridpoint`,\n`ncep_nbm_conus`, `gfs_global`) also gets a copy under `public.forecasts_prefix`\n(`/public/v1/forecasts/`), at the same path below the prefix as its private file, when the\nfile is written within 24 hours of `fetched_at` (a backfilled run included). A public file\nneeds no token:\n\n```bash\ncurl -s \"https://data.minutetemp.com/public/v1/forecasts/2026/10/12/KMDW/gfs_global/0199d2a0-0000-7000-8000-000000000002.json\"\n```\n\nIt has the same format with `hourly` cut 48 hours after `fetched_at`. Public files are\ndeleted one day after they are written, so read them within 24 hours; use the private file\nfor anything older.\n\n### Paths\n\n```\n/private/v1/forecasts/YYYY/MM/DD/{station}/{model}/{run}.json\n/public/v1/forecasts/YYYY/MM/DD/{station}/{model}/{run}.json\n```\n\n`YYYY/MM/DD` is the UTC date the run was fetched (`fetched_at`), `{station}` the station code\n(for example `KMDW`), `{model}` the model ID and `{run}` the run ID.\n\nDo not build a private path yourself: take it from `path` on the WebSocket `forecast_updated`\nevent or from `object_path` on a forecast run summary\n(`GET /api/v1/stations/{station_id}/forecast/runs`). A run without a path has no file (some\nruns stored before file delivery began); read it through the deprecated\n`GET /api/v1/stations/{station_id}/forecast/runs/{run_id}`.\n\n### Format (`minutetemp.forecast_run/v1`)\n\nThe file is the `ForecastRunFile` schema of this document:\n\n```json\n{\n  \"schema\": \"minutetemp.forecast_run/v1\",\n  \"run\": { \"id\": \"0199d2a0-0000-7000-8000-000000000001\", \"revision\": 1,\n    \"station_id\": \"5f0c1d3e-7a52-4c59-9d0b-3f1f4f1c2a10\", \"station_code\": \"KMDW\",\n    \"model_id\": \"ncep_hrrr_conus\",\n    \"forecast_time\": \"2026-10-12T05:00:00Z\", \"fetched_at\": \"2026-10-12T17:04:11.123456Z\",\n    \"timezone\": \"America/Chicago\", \"utc_offset_seconds\": -18000,\n    \"data_hash\": \"…\", \"step_seconds\": 3600, \"backfilled\": false },\n  \"units\": { \"temperature_2m_c\": \"C\", \"temperature_2m_f\": \"F\", \"wind_speed_10m\": \"km/h\",\n    \"pressure_msl\": \"hPa\", \"precipitation\": \"mm\", \"visibility\": \"m\" },\n  \"count\": 168,\n  \"hourly\": [ { \"time\": \"2026-10-12T05:00:00Z\", \"temperature_2m_c\": 11.2, \"temperature_2m_f\": 52.2,\n    \"relative_humidity_2m\": 71, \"wind_speed_10m\": 14.0, \"precipitation\": 0, \"weather_code\": 2 } ]\n}\n```\n\n- `hourly` holds every step of the run in time order, `count` of them. `step_seconds` is the\n  model's step (3600, or 900 for 15-minute models).\n- `temperature_2m_c` and `temperature_2m_f` are always present and are `null` when the model\n  gave no temperature for the step. Any other weather field is left out when it has no value;\n  zero is a value.\n- `units` gives the unit of each weather field by name. Unlike the forecast endpoints, wind\n  speeds are in km/h and the file carries fields those endpoints leave out.\n- `revision` is always 1. `backfilled` is true for a run whose file was written some time\n  after the run was fetched.\n- Parse by `schema` and ignore fields you do not know: fields may be added to `v1`.\n\n### Caching\n\nA run's file never changes once written. Private files are served with\n`Cache-Control: public, max-age=31536000, immutable` and public files with\n`Cache-Control: public, max-age=86400`, so a file you have stored never needs to be fetched\nagain. The token is checked before the CDN cache and is not part of the cache identity: a\nfile fetched again with a newer token is the same cached file, and a cached file still needs a\nvalid token. Keep the token out of your own cache keys too. `403` and `404` responses are not\ncached.\n\n## Hourly index data files\n\nThe hourly index forecast of every index city is also published as JSON files on the data\nhost `https://data.minutetemp.com`, the same host, CDN and tokens as the forecast data files.\nA city's file holds both forecast models, HRRR 15-minute (`ncep_hrrr_conus_15min`) and RRFS\n15-minute (`ncep_rrfs_conus_15min`). File requests do not count against the API rate limits.\n\n`GET /api/v1/hourly-indexes/{city}/forecast` and `GET /api/v1/hourly-indexes/{city}/forecast/history`\nare deprecated in favour of these files. They keep working with unchanged responses.\n`GET /api/v1/hourly-indexes/{city}/forecast-accuracy` and the rest of **Hourly Indexes** are not\ndeprecated.\n\n### Getting hourly index data\n\nThree pieces fit together:\n\n- The WebSocket event `index_file_updated` says a city's current file was rewritten and carries\n  its `path` and `generation`. The `hourly_index_snapshot` names the current file in `file`.\n- `GET /api/v1/data/access` returns `base_url`, the `hourly_indexes` grant for the current files\n  and one `hourly_index_days` grant per UTC date your plan may read.\n- A file's URL is `base_url` + `path` + `?` + the `query` of the grant whose `prefix` the path\n  starts with.\n\n**1. Follow live index forecasts over the WebSocket.** Connect to `/ws/api/1m` and subscribe\nwith `hourly_indexes` as described in the [WebSocket docs](/docs/ws/):\n\n```json\n{\"type\": \"subscribe\", \"hourly_indexes\": [\"chicago\"]}\n```\n\nThe `hourly_index_snapshot` carries the current file, and every rewrite arrives as:\n\n```json\n{\"type\": \"index_file_updated\", \"city\": \"chicago\", \"generation\": 88123,\n \"path\": \"/private/v1/hourly-indexes/chicago.json\", \"as_of\": \"2026-10-14T17:15:00Z\"}\n```\n\n`index_file_updated` has no `seq`: it is not part of the city's event order and is never\nreplayed by `resume`, so leave it out of gap detection. Fetch the file with the\n`hourly_indexes` grant of the access response (see **Getting forecast data** for the access\nresponse itself; `forecasts` and most day grants are left out here):\n\n```json\n{ \"data\": {\n    \"base_url\": \"https://data.minutetemp.com\",\n    \"expires_at\": \"2026-10-14T18:00:00Z\",\n    \"hourly_indexes\": { \"prefix\": \"/private/v1/hourly-indexes/\", \"query\": \"1760461200-c2lnbmF0dXJl\" },\n    \"hourly_index_days\": [\n      { \"date\": \"2026-10-13\", \"prefix\": \"/private/v1/hourly-index-days/2026/10/13/\", \"query\": \"1760461200-ZGF5MTM\" },\n      { \"date\": \"2026-10-14\", \"prefix\": \"/private/v1/hourly-index-days/2026/10/14/\", \"query\": \"1760461200-ZGF5MTQ\" } ],\n    \"public\": { \"chart_prefix\": \"/public/v1/chart/\", \"forecasts_prefix\": \"/public/v1/forecasts/\",\n                \"hourly_indexes_prefix\": \"/public/v1/hourly-indexes/\" } } }\n```\n\n```bash\ncurl -s \"https://data.minutetemp.com/private/v1/hourly-indexes/chicago.json?1760461200-c2lnbmF0dXJl\"\n```\n\nIn code:\n\n```text\nheld = {}                                   # city -\u003e the file you hold\n\non hourly_index_snapshot s with s.file:     read_current(s.city, s.file.generation)\non index_file_updated m:                    read_current(m.city, m.generation)\nevery 5 minutes, for each city:             read_current(city, 0)\n\nread_current(city, announced):\n    if access has no hourly_indexes grant: use the deprecated endpoints for now\n    file = fetch_file(\"/private/v1/hourly-indexes/\" + city + \".json\")   # as in Getting forecast data\n    if file is not a file: keep what you hold\n    if file.generation \u003c= held[city].generation: ignore it   # older or the same\n    else if now - file.as_of \u003e 1 hour: ignore it           # the publisher is behind\n    else: held[city] = file\n    if held[city].generation \u003c announced: read_current(city, announced) again in 62 seconds, once\n```\n\nThe CDN keeps a current file up to 60 seconds, so a read right after an event can return the\nprevious generation; that is what the one re-read 62 seconds later is for. The 5-minute\nrevalidation covers an event lost in a reconnect.\n\nThe socket stays authoritative for what it carries: `forecast_index` and `forecast_bias` are\nthe live forecast and its bias, and the file is authoritative for the stitched past.\nA file's `live` curve is the forecast the socket published, without bias. Use it only for a\nmodel the socket has not delivered; never let a file undo a newer `forecast_bias`, or bring\nback a model that a `forecast_index` removed.\n\n**2. Pull history for your plan window.** A day file holds one UTC date of one city, both\nmodels, at `/private/v1/hourly-index-days/YYYY/MM/DD/{city}.json`, read with the grant of its\ndate in `hourly_index_days`:\n\n```bash\ncurl -s \"https://data.minutetemp.com/private/v1/hourly-index-days/2026/10/13/chicago.json?1760461200-ZGF5MTM\"\n```\n\n```text\nfor each grant g of access.hourly_index_days:\n    day = fetch_file(g.prefix + city + \".json\")\n    if day is a file: save(day)\n    if 404 and g.date is today or yesterday: the current file covers it (span_start below)\n```\n\nA date's file is written once the date is over (from about 00:05 UTC the next day), so today has\nno day file and yesterday's may not be written yet. The private current file covers both: it\nstarts at `span_start`, 00:00 UTC yesterday (or the day before, while that day's file is not\nwritten yet), and runs to the last forecast step. A day file can be rewritten in place when a past run is\ncorrected (a higher `generation`); re-read it if you need the latest.\n\n| Plan    | UTC dates with a day-file grant   |\n|---------|-----------------------------------|\n| Starter | today and the 7 dates before it   |\n| Pro     | today and the 30 dates before it  |\n| Clanker | today and the 90 dates before it  |\n\nDay files are kept 92 days. Dates before 2026-10-12 are marked `backfilled: true` and can be\npartial: fewer member stations had every run then.\n\nEach file's series is the answer of the deprecated history endpoint for its range. Day series\nput end to end can differ from one history request across a midnight or a calibration\nchange, because a request's bias and member set are taken over its own window. For an exact\nanswer over another window, recompute from `points` (the stitched member temperatures each\nseries was built from) or ask the deprecated endpoint.\n\n**3. Refresh tokens and handle errors.** Tokens, `403`, `404` and `429` work as described in\n**Getting forecast data**, with two differences:\n\n- The current files' token covers every city's current file and is not tied to a date. It is\n  absent from the access response when the plan has no file access, while hourly index files\n  are not offered, and while the files are not known to be current. Without it, use the\n  deprecated endpoints.\n- `404` for a day file of today or yesterday means it is not written yet (see step 2); for an\n  older date, that the date has no file (expired, or no runs).\n\nIf `GET /api/v1/data/access` answers `404` (`DATA_ACCESS_NOT_ENABLED`), or has no\n`hourly_indexes` grant and no `hourly_index_days`, hourly index files are not offered: the\ndeprecated endpoints still serve the forecast.\n\n**4. Free public copies.** Index-only cities (no Kalshi hourly series yet) also have a public\ncopy of the current file under `public.hourly_indexes_prefix`, which needs no token:\n\n```bash\ncurl -s \"https://data.minutetemp.com/public/v1/hourly-indexes/dfw.json\"\n```\n\nIt has the same format without `points`, and its `stitched` series starts 24 hours before its\n`as_of`. It is rewritten at most every 15 minutes and deleted one day after it is written;\nignore a copy whose `as_of` is more than an hour old. A city that gains a Kalshi series loses its\npublic copy. There are no public day files.\n\n### Hourly index paths\n\n```\n/private/v1/hourly-indexes/{city}.json                  current file, every active index city\n/public/v1/hourly-indexes/{city}.json                   public copy, index-only cities\n/private/v1/hourly-index-days/YYYY/MM/DD/{city}.json    day file, UTC date of the steps\n```\n\n`{city}` is the index city ID of `GET /api/v1/hourly-indexes` (for example `chicago`).\n\n### Hourly index format\n\nCurrent file (`minutetemp.hourly_index/v1`):\n\n```json\n{ \"schema\": \"minutetemp.hourly_index/v1\", \"city\": \"chicago\", \"generation\": 88123,\n  \"as_of\": \"2026-10-14T17:15:00Z\", \"span_start\": \"2026-10-13T00:00:00Z\",\n  \"models\": [\n    { \"model_id\": \"ncep_hrrr_conus_15min\",\n      \"stitched\": { \"steps\": [\"2026-10-13T00:00:00Z\"], \"value_f\": [61.4], \"adjusted_f\": [61.9],\n                    \"quorum_met\": [true], \"partial\": [false], \"members\": [],\n                    \"points\": { \"stations\": [\"KMDW\"], \"steps\": [\"2026-10-13T00:00:00Z\"],\n                                \"fetched_at\": [\"2026-10-12T23:47:10Z\"], \"temp_c\": [[16.3]], \"run\": [[0]] } },\n      \"live\": { \"run_time\": \"2026-10-14T17:00:00Z\", \"fetched_at\": \"2026-10-14T17:04:11Z\",\n                \"config_version\": \"chicago-temperature-v1.0-cal-20261012\", \"missing_members\": [],\n                \"steps\": [], \"value_f\": [], \"members\": [], \"settles\": [] } } ] }\n```\n\nDay file (`minutetemp.hourly_index_day/v1`):\n\n```json\n{ \"schema\": \"minutetemp.hourly_index_day/v1\", \"city\": \"chicago\", \"date\": \"2026-10-13\",\n  \"generation\": 87990, \"as_of\": \"2026-10-14T00:05:02Z\", \"backfilled\": false,\n  \"models\": [ { \"model_id\": \"ncep_hrrr_conus_15min\", \"steps\": [], \"value_f\": [], \"adjusted_f\": [],\n                \"quorum_met\": [], \"partial\": [], \"members\": [], \"points\": {} } ] }\n```\n\n- Every model has an entry. A series (`stitched`, or a day file's model) has the fields of the\n  deprecated `GET /api/v1/hourly-indexes/{city}/forecast/history` response, aligned to `steps`;\n  a step with no member forecast is left out. A day file's series covers 00:00 to 23:45 UTC of\n  its date.\n- `points` are the stitched member temperatures (°C) the series was computed from: per station\n  of `stations`, aligned to `points.steps`, `temp_c` and `run` (an index into `fetched_at`, the\n  run the temperature came from); null where the station has none. Present in day files and\n  the private current file only.\n- `live` is the model's latest forecast curve as published on the WebSocket, without bias;\n  null when the model has no forecast now.\n- `generation` orders the files of a city (or of a city and date); `as_of` means every change\n  committed before it is in the file.\n- Parse by `schema` and ignore fields you do not know: fields may be added to `v1`.\n\n### Hourly index caching\n\nCurrent files are served with `Cache-Control: public, max-age=15, s-maxage=60`, day files with\n`public, max-age=300, s-maxage=3600`. Tokens are not part of the cache identity, as for the\nforecast files.\n","title":"MinuteTemp API","version":"1.17.0"},"openapi":"3.1.0","paths":{"/api/v1/cities":{"get":{"description":"Returns all active cities with their slugs and station metadata.","operationId":"listCities","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"count":{"type":"integer"},"data":{"items":{"$ref":"#/components/schemas/City"},"type":"array"}},"type":"object"}}},"description":"List of cities"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded"}},"summary":"List all cities","tags":["Cities"]}},"/api/v1/cities/{slug}":{"get":{"description":"Returns a single city with its stations by URL slug (e.g. chi, nyc).","operationId":"getCity","parameters":[{"example":"chi","in":"path","name":"slug","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/City"}},"type":"object"}}},"description":"City details"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"City not found"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded"}},"summary":"Get city by slug","tags":["Cities"]}},"/api/v1/data/access":{"get":{"description":"Returns the base URL of the forecast data files and one access token per path prefix the\nplan may read. Tokens are valid until `expires_at`; a response may be reused for up to five\nminutes (see its `Cache-Control`) and must not be shared between API keys. Fetch a new one\nshortly before `expires_at`, when a date you need is not listed, or after a file request\nanswers `403`. See **Getting forecast data** for the full flow.\n\nThe same response carries the hourly index file grants (`hourly_indexes`,\n`hourly_index_days`); see **Hourly index data files**.\n\nIf file delivery is switched off this endpoint answers `404` with code\n`DATA_ACCESS_NOT_ENABLED`; the deprecated forecast and hourly index forecast endpoints\nstill serve the data then.\n","operationId":"getDataAccess","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/DataAccess"}},"type":"object"}}},"description":"Base URL and access tokens","headers":{"Cache-Control":{"description":"`private, max-age=N`, N at most 300 and never past the next UTC midnight","schema":{"type":"string"}}}},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid or missing API key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The plan has no API access"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forecast data file access is not enabled (`DATA_ACCESS_NOT_ENABLED`)"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded"}},"summary":"Get access to forecast data files","tags":["Forecasts"]}},"/api/v1/forecasts/models":{"get":{"description":"Returns available weather forecast models. Paid API subscribers can fetch every model on\nforecast endpoints. The `is_public` flag marks the three models also available on the free\nweb UI (`nws_gridpoint`, `ncep_nbm_conus`, `gfs_global`); see **Plan Limits**.\n","operationId":"listForecastModels","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"count":{"type":"integer"},"data":{"items":{"$ref":"#/components/schemas/ForecastModel"},"type":"array"}},"type":"object"}}},"description":"List of models"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded"}},"summary":"List forecast models","tags":["Forecasts"]}},"/api/v1/hourly-indexes":{"get":{"description":"Every active Kalshi Weather Index city with its members, hourly series, newest minute,\ncurrent hour and open event (read from the live cache). For live updates subscribe on\nthe WebSocket with `hourly_indexes` instead of polling.\n","operationId":"listHourlyIndexes","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"items":{"$ref":"#/components/schemas/HourlyIndexCity"},"type":"array"}},"type":"object"}}},"description":"Active index cities, cities with an hourly market first"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded; use the WebSocket `hourly_indexes` subscription for live data"}},"summary":"List hourly index cities","tags":["Hourly Indexes"]}},"/api/v1/hourly-indexes/{city}":{"get":{"description":"The city's current state, the same body as the WebSocket `hourly_index_snapshot` without\n`minutes`. Use it for cold starts; live changes arrive on the WebSocket. The `ladder` lists\nthe strikes without their quotes (null since v1.16: the public API serves no market prices).\n","operationId":"getHourlyIndex","parameters":[{"$ref":"#/components/parameters/HourlyIndexCityID"}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/HourlyIndexCity"}},"type":"object"}}},"description":"City state"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unknown or inactive index city"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded; use the WebSocket `hourly_indexes` subscription for live data"}},"summary":"Get an hourly index city","tags":["Hourly Indexes"]}},"/api/v1/hourly-indexes/{city}/calibrations":{"get":{"description":"The city's calibration timeline (member weights and offsets by config version), oldest first.","operationId":"getHourlyIndexCalibrations","parameters":[{"$ref":"#/components/parameters/HourlyIndexCityID"}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"calibrations":{"items":{"$ref":"#/components/schemas/HourlyIndexCalibration"},"type":"array"},"city":{"type":"string"}},"type":"object"}},"type":"object"}}},"description":"Calibration records"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unknown or inactive index city"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded"}},"summary":"Get hourly index calibrations","tags":["Hourly Indexes"]}},"/api/v1/hourly-indexes/{city}/forecast":{"get":{"deprecated":true,"description":"Deprecated: follow the city's current hourly index data file instead (its `live` curve,\nannounced by `index_file_updated`; see **Hourly index data files**). It keeps answering as\nbelow, unchanged.\n\nThe city's latest index forecast from its members' HRRR 15-minute temperatures (the\n`forecast` field of the city body), or with `model=ncep_rrfs_conus_15min` from their RRFS\n15-minute temperatures (the city body's `forecasts` entry). Live refreshes arrive as the\nWebSocket `forecast_index` event. `forecast` is null when the model has no forecast for the\ncity; the other model's is never returned in its place. An RRFS forecast with no step left\nis answered as null.\n","operationId":"getHourlyIndexForecast","parameters":[{"$ref":"#/components/parameters/HourlyIndexCityID"},{"$ref":"#/components/parameters/HourlyIndexForecastModel"}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"city":{"type":"string"},"forecast":{"allOf":[{"$ref":"#/components/schemas/HourlyIndexForecast"}],"nullable":true},"model_id":{"description":"The forecast model answered for.","example":"ncep_hrrr_conus_15min","type":"string"}},"type":"object"}},"type":"object"}}},"description":"Latest forecast (null until the model's first run)"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unsupported model"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unknown or inactive index city"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded"}},"summary":"Get the hourly index forecast","tags":["Hourly Indexes"]}},"/api/v1/hourly-indexes/{city}/forecast-accuracy":{"get":{"description":"How the 15-minute index forecast of the model (HRRR by default) did against the settle of\neach determined hour in the window, at 60, 30 and 15 minutes before `hour_end`, for the bias-adjusted and the raw\nforecast. Each lead rebuilds the forecast as it stood then: every member's newest run\nfetched by `hour_end − lead` (stitched with the calibration effective at each step), and the\nbias against the newest index minute already official then (`official_at` at or before\n`hour_end − lead`; provisional values are not used). Hours are scored once determined (within\nabout 5 minutes); cached 5 minutes. Each model is scored from its own runs; RRFS scores\nexist only for hours determined while its index forecast was enabled.\n","operationId":"getHourlyIndexForecastAccuracy","parameters":[{"$ref":"#/components/parameters/HourlyIndexCityID"},{"$ref":"#/components/parameters/HourlyIndexForecastModel"},{"in":"query","name":"window","schema":{"default":"7d","enum":["7d","30d"],"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/HourlyIndexForecastAccuracy"}},"type":"object"}}},"description":"Forecast accuracy"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid window or unsupported model"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unknown or inactive index city"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded"}},"summary":"Get the index forecast accuracy","tags":["Hourly Indexes"]}},"/api/v1/hourly-indexes/{city}/forecast/history":{"get":{"deprecated":true,"description":"Deprecated: read the hourly index day files and the current file instead (see **Hourly\nindex data files**). It keeps answering as below, unchanged, and is the exact answer for a\nwindow that crosses a midnight or a calibration change.\n\nThe index forecast of the model (HRRR by default) as it stood when each 15-minute step in\nthe window began, from that model's stored runs only: each\nmember's temperature comes from its newest run fetched at or before the step\n(`fetched_at \u003c= step`), so a later run only replaces steps from its own fetch on.\nThe index uses the calibration effective at the step and the city's quorum. Arrays align\nto `steps`; steps with no member forecast are left out. Defaults to the 6 hours to now;\nthe window may not exceed 48 hours.\n","operationId":"getHourlyIndexForecastHistory","parameters":[{"$ref":"#/components/parameters/HourlyIndexCityID"},{"$ref":"#/components/parameters/HourlyIndexForecastModel"},{"in":"query","name":"from","schema":{"format":"date-time","type":"string"}},{"in":"query","name":"to","schema":{"format":"date-time","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"adjusted_f":{"description":"Bias-adjusted index forecast per step, as forecast: `value_f` plus the bias\nknown when the step's value was fetched: the newest index minute that was\nalready official by the newest member run's `fetched_at` (its `official_at`\nat or before it, the minute within an hour), minus the stitched forecast at\nthat minute. Official values are never revised, so this uses nothing known\nonly later; it trails the live bias by about 5 minutes (a minute is official\n5 minutes after it ends). `value_f` when no such minute exists; null when\n`value_f` is null.\n","items":{"nullable":true,"type":"number"},"type":"array"},"city":{"type":"string"},"from":{"format":"date-time","type":"string"},"members":{"items":{"$ref":"#/components/schemas/HourlyIndexForecastMember"},"type":"array"},"model_id":{"description":"The forecast model answered for.","example":"ncep_hrrr_conus_15min","type":"string"},"partial":{"description":"Some members of the step's calibration had no forecast.","items":{"type":"boolean"},"type":"array"},"quorum_met":{"items":{"type":"boolean"},"type":"array"},"steps":{"items":{"format":"date-time","type":"string"},"type":"array"},"to":{"format":"date-time","type":"string"},"value_f":{"description":"Index forecast per step; null below quorum.","items":{"nullable":true,"type":"number"},"type":"array"}},"type":"object"}},"type":"object"}}},"description":"Stitched forecast steps in the window"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid window or unsupported model"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unknown or inactive index city"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded"}},"summary":"Get the stitched hourly index forecast history","tags":["Hourly Indexes"]}},"/api/v1/hourly-indexes/{city}/match-rate":{"get":{"description":"How often our hour settlement matched Kalshi's final value over the window (cached 5 minutes).","operationId":"getHourlyIndexMatchRate","parameters":[{"$ref":"#/components/parameters/HourlyIndexCityID"},{"in":"query","name":"window","schema":{"default":"7d","enum":["7d","30d"],"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/HourlyIndexMatchRate"}},"type":"object"}}},"description":"Match rate"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unknown or inactive index city"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded"}},"summary":"Get the settlement match rate","tags":["Hourly Indexes"]}},"/api/v1/hourly-indexes/{city}/minutes":{"get":{"description":"Minutes with `from` \u003c= minute \u003c= `to`, oldest first (default: the last hour). The window is\nat most 6 hours with `include=stations` and 48 hours without, and `from` must be within\nthe plan's `max_history_days`. The last 47 hours come from the live cache, older minutes\nfrom the database (kept 180 days).\n","operationId":"getHourlyIndexMinutes","parameters":[{"$ref":"#/components/parameters/HourlyIndexCityID"},{"in":"query","name":"from","schema":{"format":"date-time","type":"string"}},{"in":"query","name":"to","schema":{"format":"date-time","type":"string"}},{"description":"Include member readings per minute.","in":"query","name":"include","schema":{"enum":["stations"],"type":"string"}},{"in":"query","name":"phase","schema":{"enum":["provisional","official","missing"],"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"city":{"type":"string"},"minutes":{"items":{"$ref":"#/components/schemas/HourlyIndexMinute"},"type":"array"}},"type":"object"}},"type":"object"}}},"description":"Minutes in the window"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid window or parameters"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unknown or inactive index city"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded; use the WebSocket `hourly_indexes` subscription for live data"}},"summary":"Get hourly index minutes","tags":["Hourly Indexes"]}},"/api/v1/hourly-indexes/{city}/settlements":{"get":{"description":"Our hour settlements with `from` \u003c= hour_end \u003c= `to`, newest first (default: the last 24\nhours; at most 31 days, within `max_history_days`), with Kalshi's final value once the\nmarket is finalized. Settlements are kept forever.\n","operationId":"getHourlyIndexSettlements","parameters":[{"$ref":"#/components/parameters/HourlyIndexCityID"},{"in":"query","name":"from","schema":{"format":"date-time","type":"string"}},{"in":"query","name":"to","schema":{"format":"date-time","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"city":{"type":"string"},"settlements":{"items":{"$ref":"#/components/schemas/HourlyIndexSettlement"},"type":"array"}},"type":"object"}},"type":"object"}}},"description":"Settlements in the window"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid window"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unknown or inactive index city"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded"}},"summary":"Get hourly index settlements","tags":["Hourly Indexes"]}},"/api/v1/limits":{"get":{"description":"Returns plan limits and current regional token bucket metadata for the authenticated user.\nStatic caps match the **Plan Limits** section (tier, rate limits, `max_history_days`,\nWebSocket subscription limits).\nLimits are shared across that user's API keys, and remaining values are post-consumption\nsnapshots for the request-serving region. Requires a paid API subscription.\n","operationId":"getLimits","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/EffectiveLimits"}},"type":"object"}}},"description":"Effective limits for the authenticated user in the request-serving region"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded"}},"summary":"Get effective limits for current authenticated user","tags":["Limits"]}},"/api/v1/markets":{"get":{"description":"Returns active weather prediction markets from Kalshi, Polymarket and ForecastEx (the\ndaily high and low contracts that Robinhood and IBKR broker), without their prices: the\npublic API serves no market prices (since v1.16).\n","operationId":"listMarkets","parameters":[{"description":"Filter by platform","in":"query","name":"platform","schema":{"enum":["kalshi","polymarket","forecastex"],"type":"string"}},{"description":"Filter by city slug (e.g. chi)","in":"query","name":"city","schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"count":{"type":"integer"},"data":{"items":{"$ref":"#/components/schemas/Market"},"type":"array"}},"type":"object"}}},"description":"List of markets"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"City not found (when `city` filter is provided)"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded"}},"summary":"List prediction markets","tags":["Markets"]}},"/api/v1/markets/{market_id}":{"get":{"operationId":"getMarket","parameters":[{"in":"path","name":"market_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Market"}},"type":"object"}}},"description":"Market details"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Market not found"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded"}},"summary":"Get market details","tags":["Markets"]}},"/api/v1/markets/{market_id}/history":{"get":{"description":"Returns the snapshot times and settlement point of a single market. The points carry no\nprices: since v1.16 the public API serves none, and `yes_price`, `no_price` and `volume`\nare null. A settlement point carries `settlement_result`.\nFor a Polymarket or ForecastEx market, only the settlement point is returned, when the\nmarket has resolved inside the requested range.\nUse `cursor` pagination for deterministic bot catch-up workflows.\nThe maximum `start`–`end` span is limited by the authenticated plan tier (`max_history_days`;\nsee **Plan Limits** and `GET /api/v1/limits`).\n","operationId":"getMarketHistory","parameters":[{"description":"Kalshi market ticker, Polymarket market ID or ForecastEx contract ID","in":"path","name":"market_id","required":true,"schema":{"type":"string"}},{"description":"Inclusive lower bound for snapshot timestamps (UTC)","in":"query","name":"start","required":false,"schema":{"format":"date-time","type":"string"}},{"description":"Inclusive upper bound for snapshot timestamps (UTC)","in":"query","name":"end","required":false,"schema":{"format":"date-time","type":"string"}},{"description":"Maximum number of rows to return","in":"query","name":"limit","required":false,"schema":{"default":500,"maximum":5000,"minimum":1,"type":"integer"}},{"description":"Opaque cursor returned by a previous page","in":"query","name":"cursor","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"count":{"type":"integer"},"market_id":{"type":"string"},"page":{"$ref":"#/components/schemas/CursorPagination"},"platform":{"enum":["kalshi","polymarket","forecastex"],"type":"string"},"points":{"items":{"$ref":"#/components/schemas/MarketHistoryPoint"},"type":"array"}},"type":"object"}},"type":"object"}}},"description":"Market snapshot page"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid cursor or time range"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Market not found"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded"}},"summary":"Get market snapshot history","tags":["Markets"]}},"/api/v1/stations":{"get":{"description":"Returns a flat list of all active weather stations with their city association.","operationId":"listStations","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"count":{"type":"integer"},"data":{"items":{"$ref":"#/components/schemas/StationFlat"},"type":"array"}},"type":"object"}}},"description":"List of stations"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded"}},"summary":"List all stations","tags":["Stations"]}},"/api/v1/stations/{station_id}":{"get":{"operationId":"getStation","parameters":[{"example":"KMDW","in":"path","name":"station_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"city":{"allOf":[{"$ref":"#/components/schemas/City"}],"description":"Parent city when the station is city-linked; null for cityless stations.","nullable":true},"station":{"$ref":"#/components/schemas/Station"}},"type":"object"}},"type":"object"}}},"description":"Station with optional parent city"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Station not found"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded"}},"summary":"Get station details","tags":["Stations"]}},"/api/v1/stations/{station_id}/forecast":{"get":{"deprecated":true,"description":"Deprecated: read forecasts as files instead (see **Getting forecast data**). Follow\n`forecast_updated` on the WebSocket for new runs, list past runs with\n`GET /api/v1/stations/{station_id}/forecast/runs`, and fetch each run's file with a token\nfrom `GET /api/v1/data/access`. This endpoint keeps working and its response is unchanged.\n\nReturns the most recent forecast run for a station across all accessible models.\nA model whose most recent run was fetched before the plan's history window\n(`max_history_days`; see **Plan Limits**) is left out; if that leaves no model, the\nresponse is `403`.\n","operationId":"getStationForecast","parameters":[{"example":"KMDW","in":"path","name":"station_id","required":true,"schema":{"type":"string"}},{"description":"Filter by specific model (e.g. ncep_hrrr_conus). Omit for all models.","in":"query","name":"model_id","schema":{"type":"string"}},{"description":"Optional ETag from a previous forecast response for conditional requests.","in":"header","name":"If-None-Match","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"city":{"properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"timezone":{"type":"string"}},"type":"object"},"count":{"type":"integer"},"forecasts":{"items":{"$ref":"#/components/schemas/ForecastBundle"},"type":"array"},"station":{"properties":{"name":{"type":"string"},"station_id":{"type":"string"},"temperature_unit":{"enum":["F","C"],"type":"string"}},"type":"object"}},"type":"object"}},"type":"object"}}},"description":"Forecast data grouped by model","headers":{"ETag":{"description":"Strong ETag for forecast payload caching.","schema":{"type":"string"}}}},"304":{"description":"Not modified (If-None-Match matched current forecast ETag)","headers":{"ETag":{"description":"Current strong ETag for the forecast payload.","schema":{"type":"string"}}}},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Every requested forecast run is older than the plan's history window"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Station or forecast not found"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded"}},"summary":"Get latest forecast","tags":["Forecasts"]}},"/api/v1/stations/{station_id}/forecast/runs":{"get":{"description":"Returns forecast run metadata for a station, optionally filtered by model and time range.\nUse `cursor` pagination for deterministic catch-up.\nThe maximum `start`–`end` span is limited by plan tier (`max_history_days`; see **Plan Limits**\nand `GET /api/v1/limits`). Runs are returned from the start of the plan's history window\n(UTC midnight, `max_history_days` days before today) even when `start` is earlier; a range\nthat ends before the window is `403`.\n","operationId":"listForecastRuns","parameters":[{"example":"KMDW","in":"path","name":"station_id","required":true,"schema":{"type":"string"}},{"description":"Optional model ID filter (e.g. ncep_hrrr_conus)","in":"query","name":"model_id","required":false,"schema":{"type":"string"}},{"description":"Inclusive lower bound for `fetched_at` (UTC)","in":"query","name":"start","required":false,"schema":{"format":"date-time","type":"string"}},{"description":"Inclusive upper bound for `fetched_at` (UTC)","in":"query","name":"end","required":false,"schema":{"format":"date-time","type":"string"}},{"in":"query","name":"limit","required":false,"schema":{"default":500,"maximum":5000,"minimum":1,"type":"integer"}},{"description":"Opaque cursor returned by a previous page","in":"query","name":"cursor","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"city":{"properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"timezone":{"type":"string"}},"type":"object"},"count":{"type":"integer"},"page":{"$ref":"#/components/schemas/CursorPagination"},"runs":{"items":{"$ref":"#/components/schemas/ForecastRunSummary"},"type":"array"},"station":{"properties":{"name":{"type":"string"},"station_id":{"type":"string"},"temperature_unit":{"enum":["F","C"],"type":"string"}},"type":"object"}},"type":"object"}},"type":"object"}}},"description":"Forecast run page"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid cursor or time range"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The requested range is older than the plan's history window"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Station not found"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded"}},"summary":"List forecast runs","tags":["Forecasts"]}},"/api/v1/stations/{station_id}/forecast/runs/{run_id}":{"get":{"deprecated":true,"description":"Deprecated: read the run's file instead (see **Getting forecast data**). Take\n`object_path` from `GET /api/v1/stations/{station_id}/forecast/runs` (or `path` from the\nWebSocket `forecast_updated` event) and fetch it with a token from\n`GET /api/v1/data/access`. The file has every step and weather field of the run. This\nendpoint keeps working and its response is unchanged; it is the way to read a run that has\nno file.\n\nReturns a specific forecast run and its hourly points.\nA run fetched before the plan's history window (`max_history_days`; see **Plan Limits**)\nis `403`.\n\nA run's points never change, so a response read from the run's file carries a strong\n`ETag` and `Cache-Control: private, max-age=86400, immutable`. Send the ETag back in\n`If-None-Match` to get `304` without a body; the history window is checked first.\n","operationId":"getForecastRun","parameters":[{"example":"KMDW","in":"path","name":"station_id","required":true,"schema":{"type":"string"}},{"in":"path","name":"run_id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"city":{"properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"timezone":{"type":"string"}},"type":"object"},"count":{"type":"integer"},"forecast_run":{"$ref":"#/components/schemas/ForecastRunSummary"},"hourly":{"items":{"$ref":"#/components/schemas/HourlyForecast"},"type":"array"},"station":{"properties":{"name":{"type":"string"},"station_id":{"type":"string"},"temperature_unit":{"enum":["F","C"],"type":"string"}},"type":"object"}},"type":"object"}},"type":"object"}}},"description":"Forecast run details","headers":{"ETag":{"description":"Strong ETag of the response.","schema":{"type":"string"}}}},"304":{"description":"Not modified (If-None-Match matched the run's ETag)","headers":{"ETag":{"description":"The run's strong ETag.","schema":{"type":"string"}}}},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The forecast run is older than the plan's history window"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Station or forecast run not found"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded"}},"summary":"Get forecast run details","tags":["Forecasts"]}},"/api/v1/stations/{station_id}/observations":{"get":{"description":"Returns observations within a time range. Maximum range is 7 days for all paid API plans.\nAll paid API subscriptions receive 1-minute resolution. See **Plan Limits**.\n","operationId":"getObservations","parameters":[{"example":"KMDW","in":"path","name":"station_id","required":true,"schema":{"type":"string"}},{"description":"Start of time range (ISO 8601)","in":"query","name":"start","required":true,"schema":{"format":"date-time","type":"string"}},{"description":"End of time range (ISO 8601)","in":"query","name":"end","required":true,"schema":{"format":"date-time","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"city":{"properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"timezone":{"type":"string"}},"type":"object"},"count":{"type":"integer"},"observations":{"items":{"$ref":"#/components/schemas/Observation"},"type":"array"},"resolution":{"description":"Observation bucket size for this response. Paid external API plans always\nreceive 1-minute points (`\"1m\"`).\n","enum":["1m"],"type":"string"},"station":{"properties":{"name":{"type":"string"},"station_id":{"type":"string"},"temperature_unit":{"enum":["F","C"],"type":"string"},"uses_nws_climate_day":{"description":"True when the station default day mode is NWS climate-day.","type":"boolean"}},"type":"object"},"tier":{"description":"Effective plan tier","type":"string"}},"type":"object"}},"type":"object"}}},"description":"Observations with city/station context"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid time range"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Station not found"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded"}},"summary":"Get historical observations","tags":["Observations"]}},"/api/v1/stations/{station_id}/observations/latest":{"get":{"description":"Returns the most recent weather observation for a station, along with daily high/low.\nThe optional `day_mode` query parameter controls how `daily_*` and\n`temperature_day_*` fields bucket the station-observed day. Weather Underground\n`wu_*` fields always use the station-local calendar day.\n\n`daily_*` fields are the current authoritative merged daily extremes for\nthis station/day. They can be changed by persisted observations and by\nofficial station reports (`cli`, `dsm`, `metar_tgroup`, `metar_6hr`) when\nreport-derived values supersede or correct the observed extreme.\n`asos_daily_*` fields are the ASOS/observation-only view and exclude\nreport-derived promotions. `wu_daily_*` fields are Weather Underground\nhistorical daily page values for the station-local calendar day.\n","operationId":"getLatestObservation","parameters":[{"example":"KMDW","in":"path","name":"station_id","required":true,"schema":{"type":"string"}},{"description":"Optional temperature-day mode for `daily_*` and `temperature_day_*` fields.\nOmit to use the station default. `calendar_day` uses local midnight-to-midnight.\n`nws_climate_day` uses local standard-time climate-day semantics.\n","in":"query","name":"day_mode","required":false,"schema":{"enum":["calendar_day","nws_climate_day"],"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"asos_daily_high_c":{"nullable":true,"type":"number"},"asos_daily_high_f":{"description":"ASOS-only daily high (excludes report-derived promotions).","nullable":true,"type":"number"},"asos_daily_low_c":{"nullable":true,"type":"number"},"asos_daily_low_f":{"description":"ASOS-only daily low (excludes report-derived promotions).","nullable":true,"type":"number"},"city":{"properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"timezone":{"type":"string"}},"type":"object"},"daily_high_c":{"description":"Authoritative merged daily high in Celsius; may include report-derived values.","nullable":true,"type":"number"},"daily_high_f":{"description":"Authoritative merged daily high; may include report-derived values.","nullable":true,"type":"number"},"daily_low_c":{"description":"Authoritative merged daily low in Celsius; may include report-derived values.","nullable":true,"type":"number"},"daily_low_f":{"description":"Authoritative merged daily low; may include report-derived values.","nullable":true,"type":"number"},"observation":{"$ref":"#/components/schemas/Observation"},"station":{"properties":{"name":{"type":"string"},"station_id":{"type":"string"},"temperature_unit":{"enum":["F","C"],"type":"string"},"uses_nws_climate_day":{"description":"True when this response's `daily_*` fields use NWS climate-day semantics.","type":"boolean"}},"type":"object"},"temperature_day_date":{"description":"Station temperature-day label used for `daily_*` values.","format":"date","type":"string"},"temperature_day_mode":{"description":"Day-bucketing mode used for `daily_*` and `temperature_day_date`.","enum":["calendar_day","nws_climate_day"],"type":"string"},"wu_current_temp_c":{"description":"Latest Weather Underground current temperature (C).","nullable":true,"type":"number"},"wu_current_temp_f":{"description":"Latest Weather Underground current temperature (F). Display only; WU readings never appear in `observations` or `latest_observation` and never affect the daily high/low.","nullable":true,"type":"number"},"wu_daily_high_c":{"description":"Weather Underground historical daily page high for the current local day (C).","nullable":true,"type":"number"},"wu_daily_high_f":{"description":"Weather Underground historical daily page high for the current local day (F).","nullable":true,"type":"number"},"wu_daily_low_c":{"description":"Weather Underground historical daily page low for the current local day (C).","nullable":true,"type":"number"},"wu_daily_low_f":{"description":"Weather Underground historical daily page low for the current local day (F).","nullable":true,"type":"number"},"wu_day_date":{"description":"Station-local calendar date used for `wu_daily_*` values.","format":"date","type":"string"},"wu_day_mode":{"description":"Day-bucketing mode used for `wu_daily_*` values.","enum":["calendar_day"],"type":"string"},"wu_fetched_at":{"description":"Timestamp MinuteTemp fetched the latest Weather Underground observation.","format":"date-time","nullable":true,"type":"string"},"wu_observation_time":{"description":"Timestamp of the latest Weather Underground observation used for these values.","format":"date-time","nullable":true,"type":"string"}},"type":"object"}},"type":"object"}}},"description":"Latest observation with daily extremes"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Station not found"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded"}},"summary":"Get latest observation","tags":["Observations"]}},"/api/v1/stations/{station_id}/oracle-score-days":{"get":{"description":"Returns per-day, per-station, per-model oracle evaluation rows so clients can fetch the\nmaximum history allowed by their plan once and derive arbitrary local rolling windows\n(for example 4, 6, 7, 14, 30, or 90 days) without additional API calls.\n\nThis endpoint preserves the existing aggregate `/oracle-scores` endpoint. Included rows\ncontain high/low signed errors and absolute errors. In formula version `oracle-v1`,\n`combined_abs_error_f = (high_abs_error_f + low_abs_error_f) / 2`.\n\nNumeric `days` is capped by plan tier (Starter 7, Pro 30, Clanker 90). Explicit `start` and\n`end` date ranges must not exceed the plan window. `days=all` is Clanker-only. If `mode` is\nomitted, rows for `overall`, `day_ahead`, and `day_of` are returned. Excluded/missing model-day\nrows are omitted by default and returned only when `include_excluded=true`.\n","operationId":"getStationOracleScoreDays","parameters":[{"example":"KMDW","in":"path","name":"station_id","required":true,"schema":{"type":"string"}},{"description":"Lookback window as a decimal integer string (scored local days), or `all` for Clanker-tier\nAPI keys to return every retained scoring day. Numeric values are capped by plan tier.\n","in":"query","name":"days","required":false,"schema":{"default":"7","type":"string"}},{"description":"Omit to return all modes.","in":"query","name":"mode","required":false,"schema":{"$ref":"#/components/schemas/OracleScoreMode"}},{"description":"Optional forecast model ID filter. Omit to return all eligible models.","in":"query","name":"model_id","required":false,"schema":{"type":"string"}},{"description":"Inclusive scored local date lower bound (`YYYY-MM-DD`). Explicit ranges are plan-capped.","in":"query","name":"start","required":false,"schema":{"format":"date","type":"string"}},{"description":"Inclusive scored local date upper bound (`YYYY-MM-DD`). Explicit ranges are plan-capped.","in":"query","name":"end","required":false,"schema":{"format":"date","type":"string"}},{"description":"Include missing/excluded model-day rows with `included=false` and `exclusion_reason`.","in":"query","name":"include_excluded","required":false,"schema":{"default":false,"type":"boolean"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/OracleScoreDaysData"}},"type":"object"}}},"description":"Daily oracle score component rows for the requested station"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid days, mode, model, or date range"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"`days=all` requires a Clanker plan"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Station not found"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded"}},"summary":"Get raw daily oracle score components","tags":["Forecasts"]}},"/api/v1/stations/{station_id}/oracle-scores":{"get":{"description":"Returns recent model-accuracy scores for a station.\nUse `mode=day_ahead` for trader-oriented day-ahead evaluation,\n`mode=day_of` for same-day forecast evaluation, and\n`rank_by=high` or `rank_by=low` to rank models for the specific market side.\nNumeric `days` is clamped by plan tier; `days=all` is Clanker-only. See **Plan Limits**.\n","operationId":"getStationOracleScores","parameters":[{"example":"KMDW","in":"path","name":"station_id","required":true,"schema":{"type":"string"}},{"description":"Lookback window as a decimal integer string (scored local days), or `all` for Clanker-tier\nAPI keys to aggregate every retained scoring day. Numeric values are clamped by plan tier\n(Starter 7, Pro 30, Clanker 90); see **Plan Limits**. `all` is rejected for non-Clanker tiers.\n","in":"query","name":"days","required":false,"schema":{"default":"7","type":"string"}},{"description":"Which forecast-run set to evaluate.","in":"query","name":"mode","required":false,"schema":{"$ref":"#/components/schemas/OracleScoreMode","default":"overall"}},{"description":"Which MAE metric determines the returned rank order.","in":"query","name":"rank_by","required":false,"schema":{"$ref":"#/components/schemas/OracleRankBy","default":"combined"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/OracleScoreData"}},"type":"object"}}},"description":"Oracle score table for the requested station"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Station not found"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded"}},"summary":"Get oracle forecast scores","tags":["Forecasts"]}},"/api/v1/stations/{station_id}/reports":{"get":{"description":"Returns station reports filtered by optional report type and date.\nRequires a paid API subscription (Starter, Pro, or Clanker); see **Plan Limits**.\nOfficial station reports can affect the authoritative `daily_*` high/low\nvalues returned by `GET /api/v1/stations/{station_id}/observations/latest`.\nUse `report_revision` and `report_updated_at` to detect corrections.\n","operationId":"getStationReports","parameters":[{"example":"KMIA","in":"path","name":"station_id","required":true,"schema":{"type":"string"}},{"description":"Optional report type filter.","in":"query","name":"type","required":false,"schema":{"$ref":"#/components/schemas/ReportType"}},{"description":"Optional local calendar date filter (YYYY-MM-DD).","in":"query","name":"date","required":false,"schema":{"format":"date","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"items":{"$ref":"#/components/schemas/StationReport"},"type":"array"}},"type":"object"}}},"description":"Report list"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid date format"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Station not found"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded"}},"summary":"Get station reports","tags":["Reports"]}},"/api/v1/stations/{station_id}/reports/history":{"get":{"description":"Returns station reports for an inclusive `report_date` range with cursor pagination.\nRows are ordered by `issuance_time` descending, then `id` descending.\nThe maximum `start`–`end` span is limited by plan tier (`max_history_days`; see **Plan Limits**\nand `GET /api/v1/limits`).\nDates are interpreted in the station's city timezone. Omitted `start`/`end` default to the last\n7 local calendar days through today.\nHistorical reports can explain later changes to the authoritative merged\n`daily_*` high/low values. Report corrections are represented by stable\n`report_id` plus incrementing `report_revision` / updated `report_updated_at`.\n","operationId":"getStationReportsHistory","parameters":[{"example":"KMIA","in":"path","name":"station_id","required":true,"schema":{"type":"string"}},{"description":"Optional report type filter (e.g. cli).","in":"query","name":"type","required":false,"schema":{"$ref":"#/components/schemas/ReportType"}},{"description":"Inclusive lower bound on `report_date` (local calendar day, YYYY-MM-DD).","in":"query","name":"start","required":false,"schema":{"format":"date","type":"string"}},{"description":"Inclusive upper bound on `report_date` (local calendar day, YYYY-MM-DD).","in":"query","name":"end","required":false,"schema":{"format":"date","type":"string"}},{"in":"query","name":"limit","required":false,"schema":{"default":500,"maximum":5000,"minimum":1,"type":"integer"}},{"description":"Opaque cursor returned by a previous page","in":"query","name":"cursor","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"city":{"properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"timezone":{"type":"string"}},"type":"object"},"count":{"type":"integer"},"page":{"properties":{"limit":{"type":"integer"},"next_cursor":{"nullable":true,"type":"string"}},"type":"object"},"reports":{"items":{"$ref":"#/components/schemas/StationReport"},"type":"array"},"station":{"properties":{"name":{"type":"string"},"station_id":{"type":"string"},"temperature_unit":{"enum":["F","C"],"type":"string"}},"type":"object"}},"type":"object"}},"type":"object"}}},"description":"Report history page"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid date range, limit, or cursor"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Reports require a paid plan"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Station not found"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded"}},"summary":"List historical station reports","tags":["Reports"]}},"/api/v1/stations/{station_id}/reports/latest":{"get":{"description":"Returns the latest report for each report type available on this station.\nRequires a paid API subscription (Starter, Pro, or Clanker); see **Plan Limits**.\nOfficial station reports can affect the authoritative `daily_*` high/low\nvalues returned by `GET /api/v1/stations/{station_id}/observations/latest`.\nUse `report_revision` and `report_updated_at` to detect corrections.\n","operationId":"getStationReportsLatest","parameters":[{"example":"KMIA","in":"path","name":"station_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"items":{"$ref":"#/components/schemas/StationReport"},"type":"array"},"report_schedules":{"allOf":[{"$ref":"#/components/schemas/ReportSchedules"}],"nullable":true}},"type":"object"}}},"description":"Latest reports plus station report schedules"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Station not found"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded"}},"summary":"Get latest reports by type","tags":["Reports"]}},"/api/v1/stations/{station_id}/weather-events":{"get":{"description":"Returns active and recently ended weather events derived from METAR/SPECI ingestion for the requested station.","operationId":"getStationWeatherEvents","parameters":[{"example":"KMIA","in":"path","name":"station_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/StationWeatherEvents"}},"type":"object"}}},"description":"Weather event state for the station"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Station not found"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded"}},"summary":"Get station weather events","tags":["Weather Events"]}},"/api/v1/today":{"get":{"description":"Returns forecast model consensus and observed high/low temperatures for\nall active stations for the current local calendar day. Data refreshes\nevery 60 seconds.\n","operationId":"getToday","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"date":{"description":"UTC date in YYYY-MM-DD format","example":"2026-03-28","type":"string"},"stations":{"items":{"properties":{"city_name":{"example":"Chicago","type":"string"},"city_slug":{"example":"chi","type":"string"},"consensus":{"nullable":true,"properties":{"high_f":{"nullable":true,"type":"number"},"low_f":{"nullable":true,"type":"number"},"model_count":{"type":"integer"},"spread_high_f":{"nullable":true,"type":"number"}},"type":"object"},"observed":{"nullable":true,"properties":{"as_of":{"format":"date-time","type":"string"},"high_f":{"nullable":true,"type":"number"},"low_f":{"nullable":true,"type":"number"}},"type":"object"},"platforms":{"example":["kalshi","polymarket"],"items":{"type":"string"},"type":"array"},"station_id":{"type":"string"},"station_name":{"example":"KMDW","type":"string"},"timezone":{"example":"America/Chicago","type":"string"}},"type":"object"},"type":"array"}},"type":"object"}},"type":"object"}}},"description":"Today's data for all active stations"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Data temporarily unavailable (cache warming)"}},"summary":"Today's consensus and observed data","tags":["Today"]}},"/api/v1/ws-ticket":{"post":{"description":"Returns a short-lived JWT ticket (90 seconds) for WebSocket authentication.\nRequires Starter, Pro, or Clanker; subscription limits are in **Plan Limits**.\nConnect to `/ws/api/1m` (recommended) or legacy `/ws/api/5m` — both deliver realtime\n1-minute events. Use WebSocket subprotocols: first `bearer`, then the ticket.\n\n```bash\n# 1. Get a ticket\nTICKET=$(curl -s -X POST -H \"X-API-Key: your_key\" \\\n  https://api.minutetemp.com/api/v1/ws-ticket | jq -r '.data.ticket')\n\n# 2. Connect via WebSocket\nwscat -c wss://api.minutetemp.com/ws/api/1m \\\n  -s bearer -s \"$TICKET\"\n```\n","operationId":"createWsTicket","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/WsTicket"}},"type":"object"}}},"description":"WebSocket ticket"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid or missing API key"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"API key does not have websocket scope for its plan"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded"}},"summary":"Get a WebSocket ticket","tags":["WebSocket"]}}},"security":[{"apiKeyHeader":[]},{"bearerAuth":[]}],"servers":[{"description":"Production","url":"https://api.minutetemp.com"}],"tags":[{"description":"City metadata and slugs for filtering","name":"Cities"},{"description":"Weather station metadata and locations","name":"Stations"},{"description":"Real-time and historical weather observations","name":"Observations"},{"description":"Official station reports (CLI, DSM, METAR)","name":"Reports"},{"description":"Active and recently ended station weather events derived from METAR/SPECI data","name":"Weather Events"},{"description":"Temperature forecasts from multiple weather models","name":"Forecasts"},{"description":"Kalshi and Polymarket prediction market data","name":"Markets"},{"description":"Kalshi Weather Index cities, per-minute index values, hour settlements and calibrations","name":"Hourly Indexes"},{"description":"WebSocket ticket for real-time push","name":"WebSocket"},{"description":"Effective API limits for the authenticated key; see Plan Limits in the API overview","name":"Limits"}]}