{"openapi":"3.1.0","info":{"title":"Trydan GB electricity API","description":"A free, keyless, read-only JSON API for Great Britain's electricity system: live\ngeneration mix, carbon intensity, interconnector flows, demand, grid frequency,\nper-station output and history back to 2009 - built entirely on public open data.\n\n**The deal**\n\n- **Free and keyless.** Every endpoint is a plain HTTPS GET.\n- **CORS-open.** `Access-Control-Allow-Origin: *` on GET - call it straight from a browser.\n- **Stable by policy.** Changes are additive-only: fields are added, never renamed or\n  removed. If a breaking change is ever unavoidable it ships as `/api/v2` with the old\n  shape kept serving.\n- **Honest degradation.** A dead upstream never fabricates a number: sections come back\n  `null` or empty with a `source_ok`-style flag, and only the Carbon Intensity backbone\n  can 503 a request.\n\n**Fair use.** Responses are edge-cached and most data refreshes every 30 minutes -\npolling live endpoints more than once a minute buys you nothing. Requests are\nrate-limited at the edge (40 requests / 10 s per IP). No SLA - this is a free public\nservice built on free public data.\n\n**Attribution.** The numbers originate from Elexon BMRS (Insights), NESO open data, the\nCarbon Intensity API (NESO + University of Oxford), Sheffield Solar PV Live, DESNZ\n(REPD/DUKES) and NOAA GFS. If you republish data you fetched here, credit those sources\nand comply with their licences - most are Open Government Licence v3.0.\n\nThe human-readable guide - stability policy, honesty notes, fair use - lives at\n[trydan.uk/developers](https://trydan.uk/developers).\n","version":"0.1.0"},"servers":[{"url":"/","description":"This origin"}],"paths":{"/api/live":{"get":{"tags":["Live"],"summary":"The live grid snapshot","description":"The current grid snapshot the dashboard polls (~every 60 s is plenty):\nembedded-inclusive mix percentages + carbon (Carbon Intensity), absolute MW totals\n(Elexon + PV Live + NESO), per-interconnector flows and national demand. Every\nenrichment degrades independently inside a 200; only the backbone can 503.","operationId":"get_live","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LiveResponse"}}}},"503":{"description":"Carbon Intensity (the backbone) unreachable - the only failure that fails this request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/history":{"get":{"tags":["History & records"],"summary":"Half-hourly mix history (up to 48 h)","description":"Half-hourly generation-mix history for the dashboard chart. Each point is\nflattened to `{from, to, <fuel>: perc}` plus an inline `demand_mw` where Elexon\noutturn covers the half-hour; a demand outage leaves the mix series intact\n(Carbon Intensity stays the only 503).","operationId":"get_history","parameters":[{"name":"hours","in":"query","required":false,"schema":{"type":"integer","maximum":48,"minimum":1,"default":24,"title":"Hours"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HistoryResponse"}}}},"503":{"description":"Carbon Intensity (the backbone) unreachable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/history/range":{"get":{"tags":["History & records"],"summary":"Deep history from the datastore (2009 → now)","description":"Deep history (Phase 8) for the explorer, served straight from the datastore - no\nupstream call (so it's edge-cacheable and never 503). Degrades to an empty series if\nthe store isn't populated yet; `400` on a malformed `from`/`to`. Bounds are normalised\nto the half-hour grid and clamped to [2009-01-01, end of the current UTC day] - the\nservable window (TRY-78); the response echoes the served bounds. The live \"now\" marker\nis a frontend overlay on `/api/live`, seamed at `coverage.through`.","operationId":"get_history_range","parameters":[{"name":"from","in":"query","required":true,"schema":{"type":"string","description":"Range start - ISO date or datetime (UTC).","title":"From"},"description":"Range start - ISO date or datetime (UTC)."},{"name":"to","in":"query","required":true,"schema":{"type":"string","description":"Range end - ISO date or datetime (UTC).","title":"To"},"description":"Range end - ISO date or datetime (UTC)."},{"name":"resolution","in":"query","required":false,"schema":{"type":"string","description":"Bucket size: auto | hh | day | week | month | year.","default":"auto","title":"Resolution"},"description":"Bucket size: auto | hh | day | week | month | year."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HistoryRangeResponse"}}}},"400":{"description":"Malformed or misordered `from`/`to` bounds.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/records":{"get":{"tags":["History & records"],"summary":"The all-time records board","description":"The records board (Phase 8), read from the precomputed datastore table. Empty\n`records: []` if the store isn't populated yet - never a 500.","operationId":"get_records","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordsResponse"}}}}}}},"/api/curtailment":{"get":{"tags":["History & records"],"summary":"The wind-curtailment ledger","description":"The wind-curtailment ledger (TRY-54): month/year/all-time volume, the farm league and\nthe monthly + daily series - all from the datastore, no upstream call.\n\nONE route serves both consumers (the Pulse-tab panel and the `/curtailment` page).\n`source_ok: false` means the datastore was unreadable; a readable store with\n`coverage.rows == 0` means the ledger simply hasn't been ingested yet. Never 503.","operationId":"get_curtailment","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CurtailmentResponse"}}}}}}},"/api/forecast":{"get":{"tags":["Forecasts"],"summary":"Forward carbon + forward demand (~48 h)","description":"Forward carbon (CI `fw48h` - the \"greenest hour\" feed) + forward national demand\n(Elexon NDF). Carbon is the backbone here: a CI failure degrades to a 503 that hides\nonly this overlay, never the main dashboard. Forward demand degrades to null\nindependently (docs/api.md).","operationId":"get_forecast","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForecastResponse"}}}},"503":{"description":"Carbon Intensity forecast unreachable - hides only this overlay, never the main dashboard routes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/fleet":{"get":{"tags":["Stations & fleet"],"summary":"The power-station fleet (GeoJSON)","description":"Power-station fleet for the map - a pre-built static GeoJSON artifact\n(`scripts/build_fleet.py`, REPD + DUKES). No upstream call at request time, so it\nis safe to cache hard. A missing artifact is a deploy error, not a degradation.","operationId":"get_fleet","responses":{"200":{"description":"GeoJSON FeatureCollection: every operational GB power station with fuel, capacity, operator and - on the ~113 transmission-metered plants only - `bm_units` ids that key `/api/station/{id}/output`. Coordinates are [lon, lat] WGS84.","content":{"application/json":{"schema":{}},"application/geo+json":{}}},"503":{"description":"The artifact is missing - a deploy error, not a degradation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/stations":{"get":{"tags":["Stations & fleet"],"summary":"The station directory (flat, filterable)","description":"The whole fleet as a flat, filterable list - the discovery surface for station\nids, feeding `/api/station/{id}` and the `/stations/{id}` pages, without the\n1.3 MB `/api/fleet` GeoJSON. Served from the committed build-time artifact: no\nupstream call at request time; the data changes only at the quarterly bake.\nFilters are lenient: junk values are ignored and the `filters` echo shows what\nactually applied (TRY-86).\n\nDeliberately a SYNC handler returning pre-shaped dicts (review 2026-08-15):\nserialising ~3,4xx rows (~800 KB) per origin miss belongs on the threadpool, not\nthe event loop, and the rows already match the response model (test-pinned) -\nre-validating thousands of pydantic models per request of a committed artifact\nwould be pure overhead.","operationId":"get_stations","parameters":[{"name":"fuel","in":"query","required":false,"schema":{"type":"string","description":"Fuel-category filter: gas|coal|nuclear|wind|solar|hydro|biomass|storage|other. Unknown values are IGNORED (never a 4xx) - check the echoed `filters`.","default":"","title":"Fuel"},"description":"Fuel-category filter: gas|coal|nuclear|wind|solar|hydro|biomass|storage|other. Unknown values are IGNORED (never a 4xx) - check the echoed `filters`."},{"name":"q","in":"query","required":false,"schema":{"type":"string","description":"Case-insensitive substring match on the station NAME. Blank = not applied.","default":"","title":"Q"},"description":"Case-insensitive substring match on the station NAME. Blank = not applied."},{"name":"has_live_feed","in":"query","required":false,"schema":{"type":"string","description":"Pass `true` for the ~113 transmission-metered stations whose output/history endpoints carry real figures, `false` for embedded capacity-only sites. Junk values are ignored, never a 4xx.","default":"","title":"Has Live Feed"},"description":"Pass `true` for the ~113 transmission-metered stations whose output/history endpoints carry real figures, `false` for embedded capacity-only sites. Junk values are ignored, never a 4xx."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StationDirectory"}}}},"503":{"description":"The fleet artifact is missing - a deploy error, not a degradation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/regions":{"get":{"tags":["Map artifacts"],"summary":"DNO region boundary polygons (GeoJSON)","description":"Static DNO region boundary polygons (slug-tagged) for the choropleth - a\npre-built artifact (`scripts/build_regions.py`, NESO). Cache hard; the live tint\nthat colours these comes from `/api/regional`.","operationId":"get_regions","responses":{"200":{"description":"GeoJSON FeatureCollection of the 14 DNO licence areas; each feature carries {id, slug, name} - `slug` joins `/api/regional`.","content":{"application/json":{"schema":{}},"application/geo+json":{}}},"503":{"description":"The artifact is missing - a deploy error, not a degradation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/coastline":{"get":{"tags":["Map artifacts"],"summary":"NW-Europe coastline backdrop (GeoJSON)","description":"Static NW-Europe coastline (Natural Earth, clipped + simplified) - the faint\nbackdrop the Phase-5 interconnector arcs land on. A pre-built artifact\n(`scripts/build_coastline.py`); cache hard. Keyless, no tile service.","operationId":"get_coastline","responses":{"200":{"description":"GeoJSON of the clipped + simplified Natural Earth coastline the map's interconnector arcs land on.","content":{"application/json":{"schema":{}},"application/geo+json":{}}},"503":{"description":"The artifact is missing - a deploy error, not a degradation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/wind-field":{"get":{"tags":["Map artifacts"],"summary":"Wind-field overlay metadata","description":"Wind-direction overlay metadata (TRY-36): the U/V grid ranges + valid time the\nanimated map layer needs. A scheduled artifact (`scripts/ingest_wind_field.py`, NOAA\nGFS 80 m wind) - WEATHER, not generation. A missing artifact 404s (never 500) so the\nopt-in overlay just stays unavailable; the rest of the map is unaffected.","operationId":"get_wind_field","responses":{"200":{"description":"The baked wind-field metadata.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WindFieldMeta"}}}},"404":{"description":"Artifact not built (yet) - the opt-in overlay is simply unavailable; never a 500.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/wind-field.png":{"get":{"tags":["Map artifacts"],"summary":"Wind-field U/V texture (PNG)","description":"The RGBA U/V texture the wind overlay samples on the GPU (R=eastward, G=northward,\ndecoded via the ranges in `/api/wind-field`). Paired artifact; same cache posture and\nsame honest 404 when unbuilt.","operationId":"get_wind_field_png","responses":{"200":{"description":"RGBA texture: R = eastward, G = northward wind, decoded via the ranges in `/api/wind-field`.","content":{"application/json":{"schema":{}},"image/png":{}}},"404":{"description":"Artifact not built (yet) - never a 500.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/regional":{"get":{"tags":["Regional"],"summary":"Live modelled mix + intensity for the 14 DNO regions","description":"Live per-DNO-region modelled mix + intensity (the choropleth tint), proxied\nfrom Carbon Intensity `/regional` through the TTL cache. A CI failure degrades to\na 503 that hides only the region layer - the rest of the dashboard is unaffected.","operationId":"get_regional","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegionalResponse"}}}},"503":{"description":"Carbon Intensity `/regional` unreachable - hides only the region layer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/regional/forecast":{"get":{"tags":["Regional"],"summary":"Forward per-region carbon (the map scrubber feed)","description":"Forward per-region tint frames for the choropleth time-scrubber (CI fw24h,\n~48 half-hours, ONE cached upstream call for every region). A CI failure degrades\nto a 503 that hides only the scrubber - the live `/api/regional` tint and the rest\nof the map are unaffected (the `/api/forecast` overlay posture).","operationId":"get_regional_forecast","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegionalForecastResponse"}}}},"503":{"description":"Carbon Intensity fw24h unreachable - hides only the scrubber.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/station/{station_id}":{"get":{"tags":["Stations & fleet"],"summary":"One station's canonical detail card","description":"The canonical per-station card (TRY-86): full metadata from the committed\nfleet artifact (fuel, tech, both capacities, operator, region, coordinates, BM\nUnits), the station's HTML page URL plus CONCRETE sub-resource URLs (agents never\ntemplate `{id}` themselves), and its active REMIT outages inlined from the same\ncached feed as `/api/outages`. `outages_ok: false` means the REMIT feed degraded\nand the list may be empty or incomplete (same semantics as `/api/outages`) - the\nartifact-backed metadata is always intact, never a 500. Renamed ids 308 to the\nlive id (the TRY-50 map).","operationId":"get_station","parameters":[{"name":"station_id","in":"path","required":true,"schema":{"type":"string","title":"Station Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StationDetail"}}}},"308":{"description":"The id was renamed by a fleet bake - follow `Location` to the live id."},"404":{"description":"Unknown station id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"The fleet artifact is missing - a deploy error, not a degradation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/station/{station_id}/output":{"get":{"tags":["Stations & fleet"],"summary":"One station's notified plan + last metered actual","description":"A station's live NOTIFIED level (Elexon PN - a near-real-time plan, not a meter)\nplus its last METERED actual (Elexon B1610, ~a week lagged). Resolves the station's\nBM Units from the pre-built fleet crosswalk; embedded sites have none and return\n`has_live_feed=false`. Each Elexon dataset degrades independently to a null figure\nwith `source_ok=false` - never a 500 (docs/api.md). Renamed ids 308 to the live id.","operationId":"get_station_output","parameters":[{"name":"station_id","in":"path","required":true,"schema":{"type":"string","title":"Station Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StationOutput"}}}},"308":{"description":"The id was renamed by a fleet bake - follow `Location` to the live id."},"404":{"description":"Unknown station id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/station/{station_id}/history":{"get":{"tags":["Stations & fleet"],"summary":"One station's settled output history + capacity factors","description":"A station's settled METERED output history (Elexon B1610, from the datastore -\nno upstream call) plus monthly and trailing-year capacity factors. Settled data\nlags ~5-10 days: `coverage.settled_through`/`lag_days` carry the honesty, and\ntrailing partially-settled days are never served as final. Days are GB settlement\ndays. Degrades to an empty series with `source_ok=false` when the store is\nunreadable, and to honest emptiness on a pre-v4 store - never a 500 (docs/api.md).\nRenamed ids 308 to the live id.","operationId":"get_station_history","parameters":[{"name":"station_id","in":"path","required":true,"schema":{"type":"string","title":"Station Id"}},{"name":"days","in":"query","required":false,"schema":{"type":"string","description":"Daily window in days, silently clamped to [7, 366].","default":"90","title":"Days"},"description":"Daily window in days, silently clamped to [7, 366]."},{"name":"hh","in":"query","required":false,"schema":{"type":"string","description":"Pass `true` to include the recent (~28-day) half-hourly series.","default":"","title":"Hh"},"description":"Pass `true` to include the recent (~28-day) half-hourly series."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StationHistory"}}}},"308":{"description":"The id was renamed by a fleet bake - follow `Location` to the live id."},"404":{"description":"Unknown station id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/station/{station_id}/curtailment":{"get":{"tags":["Stations & fleet"],"summary":"One station's wind-curtailment totals","description":"One wind farm's curtailed energy from the TRY-54 ledger: this month / this year /\nall-stored MWh plus its share of the whole ledger per bucket. MWh, never money.\nThe ledger covers transmission-connected wind only (BOALF - an honest LOWER bound),\nso `has_curtailment: false` means \"outside the ledger's scope\", NEVER \"never\ncurtailed\" - the store isn't even consulted then. Store absent/unreadable → zeros +\n`source_ok: false`; a readable pre-TRY-54 store → zeros + `source_ok: true`,\ncoverage.rows 0. Never a 500. Renamed ids 308 to the live id.","operationId":"get_station_curtailment","parameters":[{"name":"station_id","in":"path","required":true,"schema":{"type":"string","title":"Station Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StationCurtailment"}}}},"308":{"description":"The id was renamed by a fleet bake - follow `Location` to the live id."},"404":{"description":"Unknown station id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"The fleet artifact is missing - a deploy error, not a degradation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/frequency":{"get":{"tags":["System"],"summary":"Live grid frequency","description":"Live GB grid frequency (Elexon `/system/frequency`). Pure enrichment - an outage\ndegrades to `hz: null` (never a fabricated 50.0), never a 500. The `stale` flag is the\nuser-visible guard against the documented FREQ-stream staleness trap.","operationId":"get_frequency","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FrequencyResponse"}}}}}}},"/api/pulse":{"get":{"tags":["System"],"summary":"Imbalance price, NIV and the balancing feed","description":"Imbalance price + NIV system length (DISEBSP/MID) and the balancing/curtailment feed\n(BOALF + PN baseline). Each sub-block degrades INDEPENDENTLY to null; `sources_ok` names\nwhich feed is down. Never 503 - no CI backbone.","operationId":"get_pulse","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PulseResponse"}}}}}}},"/api/outages":{"get":{"tags":["System"],"summary":"Active generator outages (REMIT)","description":"Active generator outages (Elexon REMIT), joined to stations. Consumed by BOTH the\nPulse tab and the Map badge layer from this one cached route. REMIT down → empty list +\n`source_ok: false`, never a 500.","operationId":"get_outages","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutagesResponse"}}}}}}}},"components":{"schemas":{"BalancingAction":{"properties":{"id":{"type":"string","title":"Id","description":"acceptanceNumber:bmUnit - a stable key."},"bm_unit":{"type":"string","title":"Bm Unit","description":"Raw Elexon BM Unit id, e.g. 'T_SGRWO-3'."},"station":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Station","description":"Joined station name; null if the unit is not in the reverse index."},"fuel":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fuel","description":"Reverse-index fuel key; null if unknown."},"instructed_mw":{"type":"integer","title":"Instructed Mw","description":"The held/target level the unit was instructed to, MW."},"delta_mw":{"type":"integer","title":"Delta Mw","description":"Signed change from the pre-instruction baseline, MW (negative = turned down)."},"kind":{"type":"string","enum":["curtailment","reduction","increase"],"title":"Kind","description":"curtailment (wind instructed below plan) | reduction | increase."},"curtailed_mw":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Curtailed Mw","description":"Wind-below-plan volume, MW - 'instructed down', not energy lost. Null for non-curtailment actions."},"so_flag":{"type":"boolean","title":"So Flag","description":"SO-flagged (a system action, not energy balancing) - display context only."},"accepted_at":{"type":"string","title":"Accepted At","description":"UTC ISO acceptanceTime."}},"type":"object","required":["id","bm_unit","station","fuel","instructed_mw","delta_mw","kind","curtailed_mw","so_flag","accepted_at"],"title":"BalancingAction","description":"One accepted balancing instruction (Elexon BOALF), joined to a station.\n\nEvery row is an NESO *instruction* (a notified acceptance), never a meter reading -\nlabel it \"instructed\", never \"generating\". `delta_mw` is signed (negative = turned\ndown). `curtailed_mw` is only set for wind units instructed below plan (the\n\"paid to switch off\" case); it is the honest \"instructed down\" volume, not energy lost."},"BalancingBlock":{"properties":{"actions":{"items":{"$ref":"#/components/schemas/BalancingAction"},"type":"array","title":"Actions","description":"Newest-first, capped."},"total_count":{"type":"integer","title":"Total Count","description":"Actions in the window before the cap (for 'showing N of M')."},"window_minutes":{"type":"integer","title":"Window Minutes","description":"The look-back window, minutes."},"curtailment":{"$ref":"#/components/schemas/Curtailment","description":"The live wind-curtailment headline."},"as_of":{"type":"string","title":"As Of","description":"UTC ISO of the freshest acceptanceTime."}},"type":"object","required":["actions","total_count","window_minutes","curtailment","as_of"],"title":"BalancingBlock","description":"The balancing-actions feed + curtailment headline (Elexon BOALF)."},"Carbon":{"properties":{"actual":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Actual","description":"Measured intensity, gCO₂/kWh. Null until the half-hour settles."},"forecast":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Forecast","description":"Forecast intensity, gCO₂/kWh."},"index":{"anyOf":[{"type":"string","enum":["very low","low","moderate","high","very high"]},{"type":"null"}],"title":"Index","description":"Qualitative band, 'very low' … 'very high'."}},"type":"object","title":"Carbon","description":"National carbon intensity for the covered half-hour (Carbon Intensity API)."},"CarbonForecastPoint":{"properties":{"from":{"type":"string","title":"From","description":"Half-hour start, UTC ISO-8601."},"to":{"type":"string","title":"To","description":"Half-hour end, UTC ISO-8601."},"forecast":{"type":"integer","title":"Forecast","description":"Forecast intensity, gCO₂/kWh. Never null."},"index":{"anyOf":[{"type":"string","enum":["very low","low","moderate","high","very high"]},{"type":"null"}],"title":"Index","description":"Qualitative band, 'very low' … 'very high'."}},"type":"object","required":["from","to","forecast"],"title":"CarbonForecastPoint","description":"One half-hour of forward national carbon intensity (forecast only)."},"Curtailment":{"properties":{"wind_mw":{"type":"integer","title":"Wind Mw","description":"Σ curtailed MW over wind units instructed below plan right now."},"farm_count":{"type":"integer","title":"Farm Count","description":"Distinct wind units/stations currently curtailed."},"as_of":{"type":"string","title":"As Of","description":"UTC ISO of the rollup."}},"type":"object","required":["wind_mw","farm_count","as_of"],"title":"Curtailment","description":"Live wind-curtailment rollup - transmission-metered wind only (embedded excluded,\nso this is an honest LOWER bound on system wind curtailment)."},"CurtailmentLeagueRow":{"properties":{"name":{"type":"string","title":"Name","description":"Station name, or the bare BM Unit code if unjoined."},"station_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Station Id","description":"Fleet feature id (links to the station page); null if unjoined."},"bm_units":{"items":{"type":"string"},"type":"array","title":"Bm Units","description":"The BM Units rolled into this row."},"mwh":{"type":"number","title":"Mwh","description":"Curtailed energy, MWh."},"share_pct":{"type":"number","title":"Share Pct","description":"Share of the WHOLE window's total, percent - not of the listed rows."}},"type":"object","required":["name","station_id","bm_units","mwh","share_pct"],"title":"CurtailmentLeagueRow","description":"One wind farm's curtailed volume. Several BM units roll up into one station where the\ncrosswalk resolves them; an unresolved unit stands alone under its own code."},"CurtailmentPoint":{"properties":{"period":{"type":"string","title":"Period","description":"UTC calendar bucket: 'YYYY-MM' or 'YYYY-MM-DD'."},"mwh":{"type":"number","title":"Mwh","description":"Curtailed energy in the bucket, MWh."}},"type":"object","required":["period","mwh"],"title":"CurtailmentPoint","description":"One chart bucket: `period` is `YYYY-MM` (monthly) or `YYYY-MM-DD` (daily)."},"CurtailmentResponse":{"properties":{"as_of":{"type":"string","title":"As Of","description":"UTC ISO of this response."},"coverage":{"$ref":"#/components/schemas/HistoryCoverage","description":"The ledger's stored span."},"backfill_complete":{"type":"boolean","title":"Backfill Complete","description":"False while the descending backfill is still running - 'all time' then means 'since coverage.min'."},"totals":{"$ref":"#/components/schemas/CurtailmentTotals","description":"Month / year / all-time volumes, MWh."},"league":{"items":{"$ref":"#/components/schemas/CurtailmentLeagueRow"},"type":"array","title":"League","description":"All-time top 20 farms."},"league_year":{"items":{"$ref":"#/components/schemas/CurtailmentLeagueRow"},"type":"array","title":"League Year","description":"This calendar year's top 20 farms."},"series_monthly":{"items":{"$ref":"#/components/schemas/CurtailmentPoint"},"type":"array","title":"Series Monthly","description":"Every stored month."},"series_daily":{"items":{"$ref":"#/components/schemas/CurtailmentPoint"},"type":"array","title":"Series Daily","description":"Trailing 366 days."},"source_ok":{"type":"boolean","title":"Source Ok","description":"Datastore readable; false = zeroed totals. A readable-but-empty ledger is source_ok: true with coverage.rows 0 ('still filling')."}},"type":"object","required":["as_of","coverage","backfill_complete","totals","league","league_year","series_monthly","series_daily","source_ok"],"title":"CurtailmentResponse","description":"`GET /api/curtailment` - the wind-curtailment ledger (TRY-54).\n\nA pure datastore read (no upstream → edge-cacheable, never 503). Empty totals +\n`source_ok: false` while the store is absent or the ledger table has not been created\nyet. `backfill_complete` is the honesty flag on \"all time\": until the descending\nbackfill reaches the 2017 floor, `all_time_mwh` means \"since `coverage.min`\"."},"CurtailmentTotals":{"properties":{"month":{"type":"string","title":"Month","description":"The UTC calendar month the figure covers, 'YYYY-MM'."},"month_mwh":{"type":"number","title":"Month Mwh","description":"Curtailed energy that month, MWh."},"year":{"type":"string","title":"Year","description":"The UTC calendar year the figure covers, 'YYYY'."},"year_mwh":{"type":"number","title":"Year Mwh","description":"Curtailed energy that year, MWh."},"all_time_mwh":{"type":"number","title":"All Time Mwh","description":"Curtailed energy over STORED coverage, MWh - pair with coverage.min / backfill_complete."}},"type":"object","required":["month","month_mwh","year","year_mwh","all_time_mwh"],"title":"CurtailmentTotals","description":"Headline volumes. `month`/`year` name the UTC calendar buckets the figures cover, so\nthe UI never has to guess which month \"this month\" was when the response was cached."},"Demand":{"properties":{"indo_mw":{"type":"integer","title":"Indo Mw","description":"Initial National Demand Outturn, MW - GB national demand, NET of embedded generation (not comparable with the embedded-inclusive total_mw)."},"itsdo_mw":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Itsdo Mw","description":"Transmission-system outturn, MW (runs higher: includes losses, station load and exports). Null if absent on the row."},"net_import_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Net Import Pct","description":"totals.net_mw / indo_mw × 100 - net interconnector flow as a share of national demand (both transmission-level). Positive = net-importing. Null when there is no Elexon MW block."},"as_of":{"type":"string","title":"As Of","description":"UTC half-hour the figure covers."}},"type":"object","required":["indo_mw","as_of"],"title":"Demand","description":"Live demand snapshot (Phase 6, Elexon outturn).\n\n`LiveResponse.demand` is `null` when Elexon demand is unreachable (same convention as\n`mw_totals`). We deliberately do NOT report a `total_mw − demand` \"margin\": INDO is\n*net of embedded generation* while `MwTotals.total_mw` includes it, so that difference\nis a phantom ~tens-of-GW surplus (mostly embedded gen + exports), not spare capacity -\nand we have no available-capacity data for a true margin. The honest same-basis signal\nis `net_import_pct`: net interconnector flow as a share of national demand (both are\ntransmission-level), positive = GB net-importing, negative = net-exporting."},"DemandForecastPoint":{"properties":{"from":{"type":"string","title":"From","description":"Half-hour start, UTC ISO-8601."},"to":{"type":"string","title":"To","description":"Half-hour end, UTC ISO-8601."},"demand_mw":{"type":"integer","title":"Demand Mw","description":"Forecast national demand, MW."}},"type":"object","required":["from","to","demand_mw"],"title":"DemandForecastPoint","description":"One half-hour of forward national demand (Elexon NDF, boundary N)."},"ErrorResponse":{"properties":{"error":{"type":"string","title":"Error","description":"Human-readable reason, e.g. 'carbon intensity upstream unavailable'."}},"type":"object","required":["error"],"title":"ErrorResponse","description":"The body of every non-2xx JSON response.\n\nOnly the Carbon Intensity backbone can fail a request (502/503); artifact routes\n404 when unbuilt and `/api/history/range` 400s on malformed bounds. Every\nenrichment failure degrades inside a 200 instead (docs/api.md, Errors & degradation)."},"ForecastResponse":{"properties":{"timestamp":{"type":"string","title":"Timestamp","description":"When this response was built, UTC ISO-8601."},"carbon":{"items":{"$ref":"#/components/schemas/CarbonForecastPoint"},"type":"array","title":"Carbon","description":"~48 h of forward national intensity, half-hourly, ascending."},"demand":{"anyOf":[{"items":{"$ref":"#/components/schemas/DemandForecastPoint"},"type":"array"},{"type":"null"}],"title":"Demand","description":"~24 h of forward national demand (Elexon NDF). Null if NDF is unreachable - carbon is still served."}},"type":"object","required":["timestamp","carbon"],"title":"ForecastResponse","description":"`GET /api/forecast` - forward carbon + forward demand.\n\nCarbon is the backbone here (a CI failure 503s this route, hiding only the overlay);\n`demand` is `null` when Elexon forward demand is unreachable (degrades independently)."},"FrequencyPoint":{"properties":{"t":{"type":"string","title":"T","description":"UTC ISO measurementTime."},"hz":{"type":"number","title":"Hz","description":"Grid frequency, Hz."}},"type":"object","required":["t","hz"],"title":"FrequencyPoint","description":"One 15-second grid-frequency sample, for the gauge sparkline."},"FrequencyResponse":{"properties":{"hz":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Hz","description":"Latest reading, Hz. Null when unavailable - never a fabricated 50.0."},"status":{"anyOf":[{"type":"string","enum":["normal","low","high","critical"]},{"type":"null"}],"title":"Status","description":"Health band: normal (±0.2 Hz target) | low/high (inside statutory 49.5-50.5 but off-target) | critical (outside). Null when hz is null."},"as_of":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"As Of","description":"UTC ISO of the latest sample's measurementTime."},"trend":{"items":{"$ref":"#/components/schemas/FrequencyPoint"},"type":"array","title":"Trend","description":"~10 min of samples, ascending; [] when unavailable."},"stale":{"type":"boolean","title":"Stale","description":"True when the latest sample is older than the freshness threshold - never trust a confident-but-old 50.00."},"source_ok":{"type":"boolean","title":"Source Ok","description":"Elexon reachable; false = degraded, never a 500."}},"type":"object","required":["hz","status","as_of","trend","stale","source_ok"],"title":"FrequencyResponse","description":"`GET /api/frequency` - live GB grid frequency (Elexon `/system/frequency`).\n\nThe near-real-time needle. `hz`/`status` are `null` when Elexon is unreachable.\n`stale` flags a reading older than a few minutes - the user-visible guard against\nthe documented FREQ-stream staleness trap (data-sources §8b), which can serve\nplausible-but-15h-old 50 Hz values from the wrong endpoint."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HistoryCoverage":{"properties":{"min":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Min","description":"Oldest stored half-hour, UTC ISO."},"through":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Through","description":"Newest stored half-hour - the live-overlay seam."},"rows":{"type":"integer","title":"Rows","description":"Stored half-hour rows.","default":0}},"type":"object","title":"HistoryCoverage","description":"What the datastore currently holds - lets the UI label the live seam + range gaps."},"HistoryPoint":{"properties":{"from":{"type":"string","title":"From","description":"Half-hour start, UTC ISO-8601."},"to":{"type":"string","title":"To","description":"Half-hour end, UTC ISO-8601."}},"additionalProperties":true,"type":"object","required":["from","to"],"title":"HistoryPoint","description":"One half-hour, flattened to {from, to, <fuel>: perc, ...} for Recharts.\n\nPhase 6 additively rides an `demand_mw` (INDO national demand, MW) on each point\nwhere outturn is available - the `extra=\"allow\"` config carries it with no schema\nchange, and the chart draws it as a Line, never a stacked Area."},"HistoryRangePoint":{"properties":{"from":{"type":"string","title":"From","description":"Bucket start, UTC ISO-8601."},"to":{"type":"string","title":"To","description":"Bucket end boundary, UTC ISO-8601."}},"additionalProperties":true,"type":"object","required":["from","to"],"title":"HistoryRangePoint","description":"One aggregation bucket: `{from, to, <fuel>: mw|null, generation, carbon_intensity}`.\n\nMW per fuel - the frontend does the GW/% relabel (the dashboard's `UnitToggle`\nprecedent). A fuel is `null` (never `0`) for buckets before its data-available date\n(solar<2013-01, biomass<2017-11, other<2012-02), so a fabricated flat-zero history is\nnever drawn."},"HistoryRangeResponse":{"properties":{"from":{"type":"string","title":"From","description":"Served range start, UTC ISO-8601 - the request bound normalised to the half-hour grid and clamped to the servable window (2009-01-01 → now)."},"to":{"type":"string","title":"To","description":"Served range end, UTC ISO-8601 - normalised and clamped like `from`."},"resolution":{"type":"string","title":"Resolution","description":"The bucket size served: hh | day | week | month | year."},"coverage":{"$ref":"#/components/schemas/HistoryCoverage","description":"The datastore's global span."},"series":{"items":{"$ref":"#/components/schemas/HistoryRangePoint"},"type":"array","title":"Series","description":"One point per bucket; averages of MW per fuel. A fuel is null (never 0) before its data-available date."}},"type":"object","required":["from","to","resolution","coverage","series"],"title":"HistoryRangeResponse","description":"`GET /api/history/range` - deep history for the explorer (day/week/month/year).\n\nA pure datastore read (no upstream → edge-cacheable, never 503). The live \"now\" marker\nis a frontend overlay on the `/api/live` snapshot the app already polls, seamed at\n`coverage.through` - keeping this route pure and dodging a per-fuel basis mismatch."},"HistoryResponse":{"properties":{"hours":{"type":"integer","title":"Hours","description":"The window served, echoing the request (1-48)."},"series":{"items":{"$ref":"#/components/schemas/HistoryPoint"},"type":"array","title":"Series","description":"One point per half-hour: {from, to, <fuel>: perc, ...} plus an inline demand_mw where Elexon outturn covers the half-hour."}},"type":"object","required":["hours","series"],"title":"HistoryResponse","description":"`GET /api/history` - half-hourly mix history for the dashboard chart."},"Interconnector":{"properties":{"code":{"type":"string","title":"Code","description":"Elexon interconnector code, e.g. INTFR."},"label":{"type":"string","title":"Label","description":"Display name, e.g. 'France (IFA)'."},"country":{"type":"string","title":"Country","description":"Counterparty country."},"mw":{"type":"integer","title":"Mw","description":"Signed flow in MW: positive = import into GB, negative = export."},"direction":{"type":"string","enum":["import","export"],"title":"Direction","description":"Convenience view of the sign of `mw`."}},"type":"object","required":["code","label","country","mw","direction"],"title":"Interconnector","description":"A single cross-border link. mw > 0 = import into GB, < 0 = export."},"LiveResponse":{"properties":{"timestamp":{"type":"string","title":"Timestamp","description":"When this response was built, UTC ISO-8601."},"period":{"$ref":"#/components/schemas/Period","description":"The half-hour settlement window the mix covers."},"carbon":{"$ref":"#/components/schemas/Carbon","description":"National carbon intensity for the window."},"mix":{"items":{"$ref":"#/components/schemas/MixSlice"},"type":"array","title":"Mix","description":"Generation mix, sorted by perc descending. Embedded-inclusive percentages."},"interconnectors":{"items":{"$ref":"#/components/schemas/Interconnector"},"type":"array","title":"Interconnectors","description":"Per-link signed flows (+import/−export), sorted by |mw|. Empty when Elexon is unreachable (degraded, not an error)."},"totals":{"$ref":"#/components/schemas/Totals","description":"Signed/net interconnector trade totals."},"mw_totals":{"anyOf":[{"$ref":"#/components/schemas/MwTotals"},{"type":"null"}],"description":"Absolute-MW headline (Phase 2, additive). Null when Elexon (the metered base) is down - the frontend hides the GW toggle."},"demand":{"anyOf":[{"$ref":"#/components/schemas/Demand"},{"type":"null"}],"description":"National demand block (Phase 6, additive). Null when Elexon demand is unreachable."}},"type":"object","required":["timestamp","period","carbon","mix","interconnectors","totals"],"title":"LiveResponse","description":"`GET /api/live` - the snapshot the dashboard polls."},"MixPerc":{"properties":{"fuel":{"type":"string","title":"Fuel","description":"Stable fuel key (gas, wind, solar, ...)."},"perc":{"type":"number","title":"Perc","description":"Share of the region's consumption mix, percent."}},"type":"object","required":["fuel","perc"],"title":"MixPerc","description":"A fuel's percentage share within a region (no MW at regional level)."},"MixSlice":{"properties":{"fuel":{"type":"string","title":"Fuel","description":"Stable fuel key (gas, wind, solar, imports, ...)."},"label":{"type":"string","title":"Label","description":"Display name for the fuel."},"perc":{"type":"number","title":"Perc","description":"Share of generation in percent - embedded-inclusive, the source of truth for proportions. Always present."},"category":{"type":"string","enum":["renewable","fossil","low_carbon","imports","other"],"title":"Category","description":"Grouping for the renewables/low-carbon/fossil view toggles."},"low_carbon":{"type":"boolean","title":"Low Carbon","description":"Whether this fuel counts as low-carbon."},"mw":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Mw","description":"This fuel's share of the measured total in MW (perc × total_mw, largest-remainder rounded so slices sum exactly to mw_totals.total_mw) - NOT an independent meter of the fuel. Null when no absolute total could be built (Elexon down)."},"mw_source":{"anyOf":[{"type":"string","enum":["metered","metered+embedded","embedded","imports","none"]},{"type":"null"}],"title":"Mw Source","description":"Provenance of the MW figure: metered | metered+embedded | embedded | imports | none."},"mw_estimated":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Mw Estimated","description":"True when the MW figure includes a modelled/forecast embedded component (solar via PV Live, wind via NESO)."}},"type":"object","required":["fuel","label","perc","category","low_carbon"],"title":"MixSlice","description":"One fuel's share of generation.\n\n`perc` (Carbon Intensity, embedded-inclusive) is the source of truth for\nproportions and is always present. The Phase-2 `mw` fields are ADDITIVE and\noptional: `mw` is this fuel's share of the measured total (perc × total_mw,\nHamilton-rounded so every slice sums exactly to `mw_totals.total_mw`) - NOT an\nindependent metering of the fuel. It is null when no absolute total could be\nbuilt (Elexon down / older backend)."},"MonthlyCapacityFactor":{"properties":{"month":{"type":"string","title":"Month","description":"Calendar month, YYYY-MM."},"cf_pct":{"type":"number","title":"Cf Pct","description":"100 × mwh ÷ (registry capacity × covered_hours). Registry capacity is the station's `bm_capacity_mw` (Elexon generationCapacity summed over the same BM units as the numerator), NOT the REPD/DUKES site capacity."},"mwh":{"type":"number","title":"Mwh","description":"Settled energy in the month, MWh, signed."},"covered_hours":{"type":"number","title":"Covered Hours","description":"Hours actually settled in the month (Σ per-day settled half-hours ÷ 2). Compare with the month's calendar hours to judge completeness."}},"type":"object","required":["month","cf_pct","mwh","covered_hours"],"title":"MonthlyCapacityFactor","description":"One month's capacity factor on the COVERED-hours basis (settled hours only, so a\npartially-settled month is self-weighting, never biased low)."},"MwTotals":{"properties":{"total_mw":{"type":"integer","title":"Total Mw","description":"Gross 'Generation + imports' in MW - the denominator the per-slice mw derive against. NOT demand; label it 'Generation + imports'."},"generation_mw":{"type":"integer","title":"Generation Mw","description":"total_mw − import_mw_gross."},"import_mw_gross":{"type":"integer","title":"Import Mw Gross","description":"Sum of POSITIVE interconnector flows, MW."},"metered_mw":{"type":"integer","title":"Metered Mw","description":"Elexon FUELINST metered generation (pumped storage clamped ≥ 0)."},"embedded_solar_mw":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Embedded Solar Mw","description":"PV Live embedded solar, MW. Null if PV Live is down (never 0 - 0 lies)."},"embedded_wind_mw":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Embedded Wind Mw","description":"NESO embedded-wind forecast, MW. Null if NESO is down."},"pumped_storage_mw":{"type":"integer","title":"Pumped Storage Mw","description":"Raw SIGNED pumped storage, MW (negative when pumping). Informational only - never summed into total_mw."},"basis":{"type":"string","enum":["measured","partial"],"title":"Basis","description":"'measured' = every contributing source was up; 'partial' = an embedded source was down so the total is a flagged lower bound."},"as_of":{"type":"string","title":"As Of","description":"UTC ISO-8601 half-hour the embedded figures cover."},"sources_ok":{"$ref":"#/components/schemas/SourcesOk","description":"Which contributing upstreams were reachable."}},"type":"object","required":["total_mw","generation_mw","import_mw_gross","metered_mw","embedded_solar_mw","embedded_wind_mw","pumped_storage_mw","basis","as_of","sources_ok"],"title":"MwTotals","description":"Absolute-megawatt headline + its provenance (Phase 2).\n\n`total_mw` is **Generation + imports** (gross), the denominator the per-slice\n`mw` values are derived against - NOT demand, and a **lower bound** on true\nembedded-inclusive supply (it can't see the embedded biomass/CHP long tail\nthat the Carbon Intensity percentages fold in). Uses GROSS imports because the\nCarbon Intensity `imports` slice empirically tracks gross, not net (verified\nacross import/export windows - see docs/data-sources.md). The signed/net trade\nview stays in `Totals` / `interconnectors`."},"NotifiedOutput":{"properties":{"mw":{"type":"integer","title":"Mw","description":"Σ of the station's units' notified level for the current half-hour, MW, signed (pumped storage/batteries negative while pumping/charging). A PLAN, not a meter - label it 'planned/notified', never 'generating'."},"as_of":{"type":"string","title":"As Of","description":"UTC ISO of the half-hour the level covers."},"sp":{"type":"integer","title":"Sp","description":"Settlement period, 1-48 (informational)."}},"type":"object","required":["mw","as_of","sp"],"title":"NotifiedOutput","description":"PN - near-real-time NOTIFIED level (a plan, NOT metered actual output)."},"Outage":{"properties":{"id":{"type":"string","title":"Id","description":"REMIT mrid - a stable key."},"station_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Station Id","description":"Fleet feature id (for the map ring); null if uncrosswalked."},"station_name":{"type":"string","title":"Station Name","description":"Crosswalk station name, else the REMIT assetId."},"bm_unit":{"type":"string","title":"Bm Unit","description":"REMIT assetId, e.g. 'T_HEYM-2'."},"fuel":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fuel","description":"Reverse-index/REMIT fuel key; null if unknown."},"planned":{"type":"boolean","title":"Planned","description":"True when unavailabilityType == 'Planned'."},"unavailable_mw":{"type":"integer","title":"Unavailable Mw","description":"unavailableCapacity, MW off (NOT normal − available, which diverges)."},"normal_mw":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Normal Mw","description":"normalCapacity, MW (context: 'off of N')."},"available_mw":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Available Mw","description":"availableCapacity, MW."},"cause":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cause","description":"Reported cause, if any."},"start":{"type":"string","title":"Start","description":"UTC ISO eventStartTime."},"end":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"End","description":"UTC ISO eventEndTime; null = open-ended."},"lng":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Lng","description":"Joined fleet longitude (for the map); null if uncrosswalked."},"lat":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Lat","description":"Joined fleet latitude (for the map); null if uncrosswalked."},"source_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Url","description":"Elexon's canonical REMIT record for this exact event (JSON)."}},"type":"object","required":["id","station_id","station_name","bm_unit","fuel","planned","unavailable_mw","normal_mw","available_mw","cause","start","end","lng","lat","source_url"],"title":"Outage","description":"One active generator outage (Elexon REMIT), joined to a station where possible.\n\n`unavailable_mw` is the top-level `unavailableCapacity` (NOT normalCapacity−available,\nwhich diverges 3-9×). `station_id`/`lng`/`lat` are null for assets not in the fleet\ncrosswalk - those still render in the list (by `station_name` = the REMIT asset) but\nget no map ring. `planned` distinguishes maintenance from an unplanned trip."},"OutagesResponse":{"properties":{"as_of":{"type":"string","title":"As Of","description":"UTC ISO of the fetch."},"outages":{"items":{"$ref":"#/components/schemas/Outage"},"type":"array","title":"Outages","description":"Active now, deduped per asset, sorted by unavailable_mw descending."},"source_ok":{"type":"boolean","title":"Source Ok","description":"REMIT reachable; false = empty list, never a 500."}},"type":"object","required":["as_of","outages","source_ok"],"title":"OutagesResponse","description":"`GET /api/outages` - current active production outages (Elexon REMIT).\n\nConsumed by BOTH the Pulse-tab list and the Map badge layer (one cached route, two\nconsumers). `outages: []` + `source_ok: false` when REMIT is unreachable - never 503."},"Period":{"properties":{"from":{"type":"string","title":"From","description":"Window start, UTC ISO-8601."},"to":{"type":"string","title":"To","description":"Window end, UTC ISO-8601."}},"type":"object","required":["from","to"],"title":"Period","description":"The half-hour settlement window a snapshot covers."},"PriceBlock":{"properties":{"price_gbp_per_mwh":{"type":"number","title":"Price Gbp Per Mwh","description":"The single imbalance/cash-out price, £/MWh (systemSellPrice; == systemBuyPrice since P305). Never a buy/sell pair."},"net_imbalance_volume_mwh":{"type":"number","title":"Net Imbalance Volume Mwh","description":"Net Imbalance Volume, MWh, SIGNED: positive = system SHORT (NESO net-buying), negative = LONG."},"length":{"type":"string","enum":["short","long","balanced"],"title":"Length","description":"Derived from sign(NIV) with a small |NIV| < 1 balanced deadband."},"settlement_period":{"type":"integer","title":"Settlement Period","description":"Settlement period, 1-48."},"start_time":{"type":"string","title":"Start Time","description":"UTC ISO start of the settlement period."},"wholesale_gbp_per_mwh":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Wholesale Gbp Per Mwh","description":"MID APXMIDP wholesale reference, £/MWh. Null if absent/zero-volume."},"as_of":{"type":"string","title":"As Of","description":"UTC ISO the figure covers (= start_time)."}},"type":"object","required":["price_gbp_per_mwh","net_imbalance_volume_mwh","length","settlement_period","start_time","wholesale_gbp_per_mwh","as_of"],"title":"PriceBlock","description":"Settlement-period imbalance price + system length (Elexon DISEBSP + MID).\n\nSince the P305 single-price reform `systemSellPrice == systemBuyPrice`, so we expose\nONE `price_gbp_per_mwh` (the cash-out/imbalance price - never a buy/sell pair).\n`net_imbalance_volume_mwh` is signed; `length` is derived server-side from its sign so\nthe UI never re-implements the convention (+NIV = system short). Settles ~1h after the\nperiod, so the figure is always ~1 settlement period behind - surfaced via `as_of`/`sp`."},"PulseResponse":{"properties":{"timestamp":{"type":"string","title":"Timestamp","description":"When this response was built, UTC ISO-8601."},"price":{"anyOf":[{"$ref":"#/components/schemas/PriceBlock"},{"type":"null"}],"description":"Imbalance price block; null if DISEBSP is unreachable."},"balancing":{"anyOf":[{"$ref":"#/components/schemas/BalancingBlock"},{"type":"null"}],"description":"Balancing/curtailment feed; null if BOALF is unreachable."},"sources_ok":{"$ref":"#/components/schemas/PulseSourcesOk","description":"Which Elexon feeds were reachable."}},"type":"object","required":["timestamp","price","balancing","sources_ok"],"title":"PulseResponse","description":"`GET /api/pulse` - imbalance price + balancing/curtailment (Elexon, ~60s cadence).\n\nEach sub-block is independently `null` on its own upstream failure; `sources_ok` names\nwhich feed is down. Never 503 (no CI backbone) - degrade, don't collapse."},"PulseSourcesOk":{"properties":{"prices":{"type":"boolean","title":"Prices","description":"DISEBSP reachable."},"wholesale":{"type":"boolean","title":"Wholesale","description":"MID reachable (the price block can serve without it)."},"balancing":{"type":"boolean","title":"Balancing","description":"BOALF reachable."}},"type":"object","required":["prices","wholesale","balancing"],"title":"PulseSourcesOk","description":"Per-upstream health for `/api/pulse` - each sub-block degrades independently."},"RecordEvent":{"properties":{"kind":{"type":"string","title":"Kind","description":"Stable record key, e.g. 'peak_solar'."},"label":{"type":"string","title":"Label","description":"Display label."},"value":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Value","description":"The new record value in `unit`."},"unit":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Unit","description":"gco2 | mw | mwh | pct | halfhours | date."},"ts":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ts","description":"UTC ISO when the new record was set."},"previous_value":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Previous Value","description":"The beaten value."},"previous_ts":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Previous Ts","description":"UTC ISO the beaten record was set."},"detected_at":{"type":"string","title":"Detected At","description":"UTC ISO the break was detected (the feed stamp)."}},"type":"object","required":["kind","label","detected_at"],"title":"RecordEvent","description":"One detected record break (Phase 9d) - append-only upstream, newest-first here."},"RecordItem":{"properties":{"kind":{"type":"string","title":"Kind","description":"Stable record key, e.g. 'peak_wind'."},"label":{"type":"string","title":"Label","description":"Display label, e.g. 'Peak wind'."},"value":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Value","description":"The record value in `unit`; null for date-only records."},"unit":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Unit","description":"gco2 | mw | mwh | pct | halfhours | date. NB mwh is ENERGY - format GWh/MWh, never GW."},"ts":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ts","description":"UTC ISO when the record was set."},"detail":{"additionalProperties":true,"type":"object","title":"Detail","description":"Kind-specific context, e.g. {renewable_pct: 80}."},"previous_value":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Previous Value","description":"The beaten value (Phase 9d break fields, additive)."},"previous_ts":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Previous Ts","description":"UTC ISO the beaten record was set."},"broken_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Broken At","description":"Detection stamp of the latest break - drives 'NEW record'."}},"additionalProperties":true,"type":"object","required":["kind","label"],"title":"RecordItem","description":"One records-board entry (greenest half-hour, peak wind, longest coal-free run...).\n\nThe Phase-9d break fields ride along only while the record's latest detected break\nstill describes the current holder; `broken_at` (detection stamp) drives the\n\"NEW record\" badge."},"RecordsResponse":{"properties":{"generated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Generated At","description":"UTC ISO the board was last recomputed; null if empty."},"coverage":{"$ref":"#/components/schemas/HistoryCoverage","description":"The datastore's global span."},"records":{"items":{"$ref":"#/components/schemas/RecordItem"},"type":"array","title":"Records","description":"The all-time records board."},"events":{"items":{"$ref":"#/components/schemas/RecordEvent"},"type":"array","title":"Events","description":"Recent detected breaks, newest first, capped."}},"type":"object","required":["coverage","records"],"title":"RecordsResponse","description":"`GET /api/records` - the precomputed board. `records: []` if the store is empty.\n`events` lists recent detected breaks (additive, may be empty)."},"RegionForecastCell":{"properties":{"forecast":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Forecast","description":"Forecast intensity, gCO₂/kWh (modelled)."},"index":{"anyOf":[{"type":"string","enum":["very low","low","moderate","high","very high"]},{"type":"null"}],"title":"Index","description":"Qualitative band, 'very low' … 'very high'."},"renewable_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Renewable Pct","description":"Wind + solar + hydro + biomass share, percent. Null when the window carried no mix (never a fabricated 0)."}},"type":"object","title":"RegionForecastCell","description":"One half-hour of one region's forward tint. All-null when CI omitted the region\nfor that window - an honest gap the map renders untinted, never a fabricated 0."},"RegionForecastSeries":{"properties":{"id":{"type":"integer","title":"Id","description":"Carbon Intensity regionid, 1-14."},"slug":{"type":"string","title":"Slug","description":"Join key to the boundary GeoJSON (`/api/regions`)."},"name":{"type":"string","title":"Name","description":"Region display name."},"series":{"items":{"$ref":"#/components/schemas/RegionForecastCell"},"type":"array","title":"Series","description":"Aligned index-for-index with the response's `steps`."}},"type":"object","required":["id","slug","name","series"],"title":"RegionForecastSeries","description":"One DNO region's forward series - `series[i]` covers `steps[i]` exactly."},"RegionIntensity":{"properties":{"forecast":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Forecast","description":"Forecast intensity, gCO₂/kWh (modelled)."},"index":{"anyOf":[{"type":"string","enum":["very low","low","moderate","high","very high"]},{"type":"null"}],"title":"Index","description":"Qualitative band, 'very low' … 'very high'."}},"type":"object","title":"RegionIntensity","description":"Regional carbon intensity - forecast only (no settled `actual` at this level)."},"RegionLive":{"properties":{"id":{"type":"integer","title":"Id","description":"Carbon Intensity regionid, 1-14."},"slug":{"type":"string","title":"Slug","description":"Join key to the boundary GeoJSON (`/api/regions`)."},"name":{"type":"string","title":"Name","description":"Region display name."},"intensity":{"$ref":"#/components/schemas/RegionIntensity","description":"Modelled intensity (forecast-only)."},"renewable_pct":{"type":"number","title":"Renewable Pct","description":"Derived wind + solar + hydro + biomass share, percent."},"mix":{"items":{"$ref":"#/components/schemas/MixPerc"},"type":"array","title":"Mix","description":"The region's modelled consumption mix."}},"type":"object","required":["id","slug","name","intensity","renewable_pct","mix"],"title":"RegionLive","description":"One DNO region's live snapshot, keyed by `slug` to its static polygon."},"RegionalForecastResponse":{"properties":{"timestamp":{"type":"string","title":"Timestamp","description":"When this response was built, UTC ISO-8601."},"modelled":{"type":"boolean","title":"Modelled","description":"Always true: forecast-only + ML-modelled - label it 'modelled'."},"steps":{"items":{"$ref":"#/components/schemas/Period"},"type":"array","title":"Steps","description":"~48 half-hour windows, ascending; frame 0 ≈ now."},"regions":{"items":{"$ref":"#/components/schemas/RegionForecastSeries"},"type":"array","title":"Regions","description":"Always all 14 DNO regions, sorted by id, each with a full-length series."}},"type":"object","required":["timestamp","modelled","steps","regions"],"title":"RegionalForecastResponse","description":"`GET /api/regional/forecast` - forward tint frames for the region scrubber.\n\nEvery one of the 14 regions is always present with a series aligned index-for-index\nto `steps`, so the frontend can scrub by position without joining timestamps."},"RegionalResponse":{"properties":{"timestamp":{"type":"string","title":"Timestamp","description":"When this response was built, UTC ISO-8601."},"modelled":{"type":"boolean","title":"Modelled","description":"Always true: regional figures are forecast-only + ML-modelled - label them 'modelled', never 'measured'."},"regions":{"items":{"$ref":"#/components/schemas/RegionLive"},"type":"array","title":"Regions","description":"The 14 DNO regions."}},"type":"object","required":["timestamp","modelled","regions"],"title":"RegionalResponse","description":"`GET /api/regional` - live tint for the 14-DNO choropleth."},"SettledOutput":{"properties":{"mw":{"type":"integer","title":"Mw","description":"Σ across the station's units for the latest settled half-hour, MW, signed."},"as_of":{"type":"string","title":"As Of","description":"UTC ISO halfHourEndTime the figure covers."},"age_days":{"type":"integer","title":"Age Days","description":"Whole days between `as_of` and now - staleness is mandatory to show."}},"type":"object","required":["mw","as_of","age_days"],"title":"SettledOutput","description":"B1610 - last METERED actual output. Lags ~a week; always show the age."},"SourcesOk":{"properties":{"elexon":{"type":"boolean","title":"Elexon","description":"Elexon FUELINST (the metered base) reachable."},"pvlive":{"type":"boolean","title":"Pvlive","description":"Sheffield Solar PV Live (embedded solar) reachable."},"neso":{"type":"boolean","title":"Neso","description":"NESO embedded-wind forecast reachable."}},"type":"object","required":["elexon","pvlive","neso"],"title":"SourcesOk","description":"Per-upstream health for the MW block - drives the UI's degradation badges."},"StationCapacityFactor":{"properties":{"monthly":{"items":{"$ref":"#/components/schemas/MonthlyCapacityFactor"},"type":"array","title":"Monthly","description":"Per-month capacity factors, ascending, covered-hours basis."},"trailing_year":{"anyOf":[{"$ref":"#/components/schemas/TrailingYearCapacityFactor"},{"type":"null"}],"description":"The trailing-year figure; null until 12 consecutive complete months exist."}},"type":"object","required":["monthly","trailing_year"],"title":"StationCapacityFactor","description":"Capacity factors for one station. The whole block is null for storage stations -\ntheir settled energy is a NET flow (pumping subtracts), so a load factor over it\nwould be meaningless, not merely imprecise."},"StationCurtailment":{"properties":{"id":{"type":"string","title":"Id","description":"Fleet feature id (`/api/fleet` properties.id)."},"name":{"type":"string","title":"Name","description":"Station display name."},"has_curtailment":{"type":"boolean","title":"Has Curtailment","description":"True iff transmission-connected wind - the ledger's scope (BOALF, an honest LOWER bound). False = outside that scope, NOT 'never curtailed'; totals are then definitionally zero and the store is never consulted."},"bm_units":{"items":{"type":"string"},"type":"array","title":"Bm Units","description":"The BM Units summed - the fleet crosswalk unioned with the ledger's ingest-time attribution; [] when out of scope."},"totals":{"$ref":"#/components/schemas/StationCurtailmentTotals","description":"Month / year / all-time volumes + ledger shares, MWh."},"coverage":{"$ref":"#/components/schemas/HistoryCoverage","description":"The ledger's stored span for THIS station's units."},"backfill_complete":{"type":"boolean","title":"Backfill Complete","description":"Ledger-wide backfill flag (as on `/api/curtailment`): false = 'all time' means 'since coverage.min'. Always false when out of scope or the store is unreadable (the ledger wasn't consulted)."},"source_ok":{"type":"boolean","title":"Source Ok","description":"Datastore readable; false = zeroed totals, never a 500. An out-of-scope station is source_ok: true - absence of a ledger is not a failure."}},"type":"object","required":["id","name","has_curtailment","bm_units","totals","coverage","backfill_complete","source_ok"],"title":"StationCurtailment","description":"`GET /api/station/{id}/curtailment` - one wind farm's curtailment totals (TRY-86).\n\nA pure datastore read over the TRY-54 ledger. The unit set summed is the fleet\ncrosswalk UNIONED with the ledger's own ingest-time attribution, so a quarterly\nbake drifting the crosswalk can't silently drop stored rows."},"StationCurtailmentTotals":{"properties":{"month":{"type":"string","title":"Month","description":"The UTC calendar month the figures cover, 'YYYY-MM'."},"month_mwh":{"type":"number","title":"Month Mwh","description":"This station's curtailed energy that month, MWh."},"month_share_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Month Share Pct","description":"Share of ALL stored transmission-wind curtailment that month, percent; null when the ledger-wide bucket is 0."},"year":{"type":"string","title":"Year","description":"The UTC calendar year the figures cover, 'YYYY'."},"year_mwh":{"type":"number","title":"Year Mwh","description":"This station's curtailed energy that year, MWh."},"year_share_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Year Share Pct","description":"Share of ALL stored transmission-wind curtailment that year, percent; null when the ledger-wide bucket is 0."},"all_time_mwh":{"type":"number","title":"All Time Mwh","description":"Curtailed energy over STORED coverage, MWh - pair with coverage.min / backfill_complete."},"all_time_share_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"All Time Share Pct","description":"Share of the whole stored ledger, percent; null when the ledger is empty."}},"type":"object","required":["month","month_mwh","month_share_pct","year","year_mwh","year_share_pct","all_time_mwh","all_time_share_pct"],"title":"StationCurtailmentTotals","description":"One station's curtailed volumes plus its share of the whole ledger per bucket."},"StationDailyPoint":{"properties":{"day":{"type":"string","title":"Day","description":"GB settlement day, YYYY-MM-DD (a local calendar day)."},"mwh":{"type":"number","title":"Mwh","description":"Settled energy that day, MWh, summed over the station's BM units. NET and signed: pumped storage/batteries are negative while pumping/charging."},"peak_mw":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Peak Mw","description":"The station's highest settled half-hour that day, MW - derivable only while half-hourly rows are retained (~60 days), null for older days."}},"type":"object","required":["day","mwh","peak_mw"],"title":"StationDailyPoint","description":"One settled day of a station's metered output."},"StationDetail":{"properties":{"id":{"type":"string","title":"Id","description":"Stable station id - also the `/stations/{id}` page slug (`/api/fleet` properties.id)."},"name":{"type":"string","title":"Name","description":"Station display name."},"fuel":{"type":"string","enum":["gas","coal","nuclear","wind","solar","hydro","biomass","storage","other"],"title":"Fuel","description":"Fleet fuel category (the map taxonomy)."},"tech":{"type":"string","title":"Tech","description":"Source technology string as the dataset lists it (e.g. 'CCGT', 'Solar Photovoltaics', 'Wind Offshore')."},"capacity_mw":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Capacity Mw","description":"REPD/DUKES site capacity, MW; null where the source lists none. NOT the Elexon registry capacity (`bm_capacity_mw` below - two figures, never blended)."},"operator":{"type":"string","title":"Operator","description":"Operator/developer name as the source dataset lists it."},"country":{"type":"string","title":"Country","description":"England | Scotland | Wales | Northern Ireland."},"source":{"type":"string","title":"Source","description":"'repd' (renewables + storage) | 'dukes' (conventional) - which dataset the station came from, for attribution."},"region_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Region Slug","description":"DNO region slug (joins `/api/regional`); null offshore or unmatched."},"region_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Region Name","description":"DNO region display name; null offshore or unmatched."},"lat":{"type":"number","title":"Lat","description":"Latitude, WGS84."},"lng":{"type":"number","title":"Lng","description":"Longitude, WGS84."},"has_live_feed":{"type":"boolean","title":"Has Live Feed","description":"True iff the station has transmission BM Units (PN/B1610 feeds). False = embedded/capacity-only - an honest 'no live feed', not a failure."},"bm_units":{"items":{"type":"string"},"type":"array","title":"Bm Units","description":"The station's Elexon BM Unit ids; [] if embedded."},"bm_capacity_mw":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Bm Capacity Mw","description":"Registry capacity (Elexon generationCapacity summed over the same BM units) - the capacity-factor denominator. Null when the station has no transmission units or the crosswalk carries none."},"page":{"type":"string","title":"Page","description":"Relative URL of the station's HTML page - `/stations/{id}`."},"links":{"$ref":"#/components/schemas/StationLinks","description":"Concrete sub-resource URLs for this station - no templating needed."},"outages":{"items":{"$ref":"#/components/schemas/Outage"},"type":"array","title":"Outages","description":"ACTIVE REMIT outages for THIS station, unavailable_mw descending. Same cached feed and semantics as `/api/outages`: when `outages_ok` is false the list may be empty or incomplete - never invented, never a 500."},"outages_ok":{"type":"boolean","title":"Outages Ok","description":"The REMIT feed was fetched completely; false = a feed outage or partial detail-fetch failures - treat `outages` as possibly incomplete. The artifact-backed metadata above is always intact."}},"type":"object","required":["id","name","fuel","tech","capacity_mw","operator","country","source","region_slug","region_name","lat","lng","has_live_feed","bm_units","bm_capacity_mw","page","links","outages","outages_ok"],"title":"StationDetail","description":"`GET /api/station/{id}` - the canonical station card (TRY-86).\n\nMetadata straight from the committed fleet artifact (so it cannot degrade at\nruntime), plus the station's ACTIVE REMIT outages inlined from the same cached\nfeed as `/api/outages` - the one part that can degrade, flagged by `outages_ok`."},"StationDirectory":{"properties":{"generated":{"type":"string","title":"Generated","description":"Fleet artifact bake date (YYYY-MM-DD) - the data's vintage; changes only on a deploy carrying a new bake."},"count":{"type":"integer","title":"Count","description":"Stations returned after filtering."},"total":{"type":"integer","title":"Total","description":"Stations in the whole artifact, unfiltered."},"filters":{"$ref":"#/components/schemas/StationDirectoryFilters","description":"The filters actually applied, post-normalisation."},"stations":{"items":{"$ref":"#/components/schemas/StationDirectoryRow"},"type":"array","title":"Stations","description":"Matching stations, site capacity descending (nulls last), name ascending."}},"type":"object","required":["generated","count","total","filters","stations"],"title":"StationDirectory","description":"`GET /api/stations` - the flat, filterable station directory (TRY-86).\n\nThe fleet artifact minus the GeoJSON: the cheap way for API consumers to discover\nstation ids without the 1.3 MB `/api/fleet` blob. Data changes only when a deploy\nships a new quarterly bake."},"StationDirectoryFilters":{"properties":{"fuel":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fuel","description":"Applied fuel category; null = not applied (blank or unknown value)."},"q":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Q","description":"Applied case-insensitive name-substring query; null = not applied."},"has_live_feed":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Has Live Feed","description":"Applied live-feed filter; null = not applied (blank or junk value)."}},"type":"object","required":["fuel","q","has_live_feed"],"title":"StationDirectoryFilters","description":"The filters as APPLIED - the junk-degradation signal: an unrecognised value is\nignored (never a 4xx) and echoes null here, so a caller can see a filter that\nsilently didn't take."},"StationDirectoryRow":{"properties":{"id":{"type":"string","title":"Id","description":"Stable station id - the `/api/station/{id}` key AND the `/stations/{id}` page slug (`/api/fleet` properties.id)."},"name":{"type":"string","title":"Name","description":"Station display name."},"fuel":{"type":"string","enum":["gas","coal","nuclear","wind","solar","hydro","biomass","storage","other"],"title":"Fuel","description":"Fleet fuel category (the map taxonomy)."},"capacity_mw":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Capacity Mw","description":"REPD/DUKES site capacity, MW; null where the source lists none. NOT the Elexon registry capacity (`bm_capacity_mw` on the detail card)."},"operator":{"type":"string","title":"Operator","description":"Operator/developer name as the source dataset lists it."},"country":{"type":"string","title":"Country","description":"England | Scotland | Wales | Northern Ireland."},"region_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Region Slug","description":"DNO region slug (joins `/api/regional`); null offshore or unmatched."},"has_live_feed":{"type":"boolean","title":"Has Live Feed","description":"True iff the station has transmission BM Units - the ~113 whose `/api/station/{id}/output` and `/history` carry real figures. False = embedded/capacity-only."},"lat":{"type":"number","title":"Lat","description":"Latitude, WGS84."},"lng":{"type":"number","title":"Lng","description":"Longitude, WGS84."}},"type":"object","required":["id","name","fuel","capacity_mw","operator","country","region_slug","has_live_feed","lat","lng"],"title":"StationDirectoryRow","description":"One station in the directory - the discovery surface for station ids."},"StationHalfHourPoint":{"properties":{"ts":{"type":"string","title":"Ts","description":"UTC ISO half-hour start (the `/api/history` key space)."},"mw":{"type":"number","title":"Mw","description":"Settled MW, summed over the station's units, signed."}},"type":"object","required":["ts","mw"],"title":"StationHalfHourPoint","description":"One settled half-hour of a station's metered output (recent window only)."},"StationHistory":{"properties":{"id":{"type":"string","title":"Id","description":"Fleet feature id (`/api/fleet` properties.id)."},"name":{"type":"string","title":"Name","description":"Station display name."},"has_history":{"type":"boolean","title":"Has History","description":"True iff the station has transmission BM Units (B1610-metered). False = embedded/capacity-only - an honest 'no meter', not a failure."},"bm_units":{"items":{"type":"string"},"type":"array","title":"Bm Units","description":"The station's Elexon BM Unit ids; [] if embedded."},"capacity_mw":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Capacity Mw","description":"Registry capacity (`bm_capacity_mw`) - the capacity-factor denominator. Null when the crosswalk carries none."},"days":{"type":"integer","title":"Days","description":"Daily-series window served, echoed after clamping to [7, 366]."},"daily":{"items":{"$ref":"#/components/schemas/StationDailyPoint"},"type":"array","title":"Daily","description":"Settled days, ascending, windowed by `days`. Missing days are gaps (not settled / station idle before its meters existed), never zeros."},"halfhourly":{"anyOf":[{"items":{"$ref":"#/components/schemas/StationHalfHourPoint"},"type":"array"},{"type":"null"}],"title":"Halfhourly","description":"The recent settled half-hours (~28 days), only when `?hh=true`; null otherwise. Older half-hours are not retained."},"capacity_factor":{"anyOf":[{"$ref":"#/components/schemas/StationCapacityFactor"},{"type":"null"}],"description":"Monthly + trailing-year capacity factors; null for storage stations (net flow) and when no registry capacity exists."},"coverage":{"$ref":"#/components/schemas/StationHistoryCoverage","description":"Store coverage for this station."},"backfill_complete":{"type":"boolean","title":"Backfill Complete","description":"True once the 2023-floor backfill has finished; false = the series is still filling and 'all history' is honestly 'since coverage.min_day'."},"source_ok":{"type":"boolean","title":"Source Ok","description":"The datastore was readable; false = everything degraded to empty, never a 500. A readable store with 0 rows just hasn't ingested yet."}},"type":"object","required":["id","name","has_history","bm_units","capacity_mw","days","daily","halfhourly","capacity_factor","coverage","backfill_complete","source_ok"],"title":"StationHistory","description":"`GET /api/station/{id}/history` - settled output history + capacity factors."},"StationHistoryCoverage":{"properties":{"min_day":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Min Day","description":"Earliest settled day stored; null if none."},"settled_through":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Settled Through","description":"Latest COMPLETE settled day (trailing partially-settled days are dropped, so a mid-publication day is never presented as final)."},"lag_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Lag Days","description":"Whole days between `settled_through` and today - the settlement lag, mandatory to show next to any 'latest' figure."},"rows":{"type":"integer","title":"Rows","description":"Settled station-days stored for this station."}},"type":"object","required":["min_day","settled_through","lag_days","rows"],"title":"StationHistoryCoverage","description":"What the store holds for THIS station - the honesty block."},"StationLinks":{"properties":{"output":{"type":"string","title":"Output","description":"`/api/station/{id}/output` - live notified plan + last metered actual. Valid for every station (embedded ones answer `has_live_feed: false`)."},"history":{"type":"string","title":"History","description":"`/api/station/{id}/history` - settled daily output + capacity factors. Valid for every station (embedded ones answer `has_history: false`)."},"curtailment":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Curtailment","description":"`/api/station/{id}/curtailment` - the station's wind-curtailment ledger totals; null for stations outside the ledger's scope (non-wind or embedded - the endpoint would answer `has_curtailment: false`)."}},"type":"object","required":["output","history","curtailment"],"title":"StationLinks","description":"Concrete relative same-origin URLs for a station's sub-resources, so agents\nnever have to template `{id}` into a path themselves."},"StationOutput":{"properties":{"id":{"type":"string","title":"Id","description":"Fleet feature id (`/api/fleet` properties.id)."},"name":{"type":"string","title":"Name","description":"Station display name."},"has_live_feed":{"type":"boolean","title":"Has Live Feed","description":"True iff the station has transmission BM Units (PN/B1610). False = embedded/capacity-only - an honest 'no live feed', not a failure."},"bm_units":{"items":{"type":"string"},"type":"array","title":"Bm Units","description":"The station's Elexon BM Unit ids; [] if embedded."},"notified":{"anyOf":[{"$ref":"#/components/schemas/NotifiedOutput"},{"type":"null"}],"description":"Elexon PN - a near-real-time PLAN, not a meter. Null if the station has no feed or PN is unavailable."},"settled":{"anyOf":[{"$ref":"#/components/schemas/SettledOutput"},{"type":"null"}],"description":"Elexon B1610 - metered actual, lagging ~a week. Null if the station has no feed or B1610 is unavailable."},"source_ok":{"type":"boolean","title":"Source Ok","description":"Elexon reachable; false = figures degraded to null, never a 500."}},"type":"object","required":["id","name","has_live_feed","bm_units","notified","settled","source_ok"],"title":"StationOutput","description":"`GET /api/station/{id}/output` - a station's live notified level + last actual."},"Totals":{"properties":{"import_mw":{"type":"integer","title":"Import Mw","description":"Sum of positive (importing) flows, MW."},"export_mw":{"type":"integer","title":"Export Mw","description":"Magnitude of negative (exporting) flows, MW."},"net_mw":{"type":"integer","title":"Net Mw","description":"import_mw − export_mw; positive = GB net-importing, negative = net-exporting."}},"type":"object","required":["import_mw","export_mw","net_mw"],"title":"Totals","description":"Interconnector trade totals - the signed/NET view.\n\nSummed over `interconnectors`. Deliberately a different view from\n`mw_totals.import_mw_gross` (the gross supply share the mix derives against):\nmix = gross supply, totals = signed net trade. Do not reconcile or sum them.\nZeroed when Elexon is unreachable (and `interconnectors` is empty)."},"TrailingYearCapacityFactor":{"properties":{"cf_pct":{"type":"number","title":"Cf Pct","description":"Capacity factor over the 12 months, percent."},"from_month":{"type":"string","title":"From Month","description":"First month of the window, YYYY-MM."},"to_month":{"type":"string","title":"To Month","description":"Last month of the window, YYYY-MM."}},"type":"object","required":["cf_pct","from_month","to_month"],"title":"TrailingYearCapacityFactor","description":"The latest 12 CONSECUTIVE complete months (complete = ≥90% of calendar hours\nsettled). Null while the backfill is still filling or after a coverage gap."},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"WindFieldMeta":{"properties":{"width":{"type":"integer","title":"Width","description":"Grid columns (west→east) = PNG width."},"height":{"type":"integer","title":"Height","description":"Grid rows (north→south) = PNG height."},"bbox":{"items":{"type":"number"},"type":"array","title":"Bbox","description":"[west, south, east, north] lon/lat the grid spans."},"u_min":{"type":"number","title":"U Min","description":"Eastward wind, m/s, decoded from R=0."},"u_max":{"type":"number","title":"U Max","description":"Eastward wind, m/s, decoded from R=255."},"v_min":{"type":"number","title":"V Min","description":"Northward wind, m/s, decoded from G=0."},"v_max":{"type":"number","title":"V Max","description":"Northward wind, m/s, decoded from G=255."},"speed_max":{"type":"number","title":"Speed Max","description":"Max |wind| in the grid, m/s - the legend ramp top."},"level":{"type":"string","title":"Level","description":"Hub-height PROXY (GFS has no 100 m field) - label it '~80 m'.","default":"80m"},"weather_model":{"type":"string","title":"Weather Model","description":"Source NWP model - this is WEATHER, not generation.","default":"GFS"},"cycle":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cycle","description":"Model run, UTC ISO (e.g. 2026-07-17T12:00:00Z)."},"valid_time":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Valid Time","description":"Forecast valid instant, UTC ISO - show it like any reading."},"image":{"type":"string","title":"Image","description":"The paired texture URL, cycle-versioned so it can cache long.","default":"/api/wind-field.png"},"source_ok":{"type":"boolean","title":"Source Ok","description":"A present artifact implies true; a missing artifact 404s (never 500).","default":true}},"type":"object","required":["width","height","bbox","u_min","u_max","v_min","v_max","speed_max"],"title":"WindFieldMeta","description":"Describes the RGBA U/V texture served at `/api/wind-field.png`.\n\nThe texture packs eastward (`R`) and northward (`G`) wind components, linearly scaled\nover `[u_min,u_max]` / `[v_min,v_max]`; the frontend decodes them back to m/s with\nthese ranges. The grid is regular lat/lon covering `bbox` ([west, south, east, north]),\nrows ordered north→south and columns west→east (so PNG row 0 = the northern edge).\n\nHonesty (CLAUDE.md ethos): `level` is the hub-height PROXY - GFS 0.25° has no 100 m\nfield, so we use 80 m above ground and say so; `valid_time` is the model's valid\ninstant (a forecast/analysis, not a live meter) and must be surfaced like any reading."}}},"tags":[{"name":"Live","description":"The current grid snapshot the dashboard polls."},{"name":"History & records","description":"Half-hourly history, the deep-history datastore (2009 → now), the all-time records board and the wind-curtailment ledger."},{"name":"Forecasts","description":"Forward carbon intensity + forward national demand - the greenest-hour feed."},{"name":"Regional","description":"The 14 DNO regions: modelled live mix/intensity and the forward scrubber feed. Forecast-only + ML-modelled - always label it 'modelled'."},{"name":"Stations & fleet","description":"The power-station fleet artifact and per-station live output (a notified PLAN + a ~week-lagged settled meter, never conflated)."},{"name":"System","description":"Grid frequency, imbalance price / NIV system length, balancing actions and REMIT generator outages."},{"name":"Map artifacts","description":"Static GeoJSON/PNG artifacts the map renders: region polygons, coastline, the wind-field overlay."}]}