You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This will be deployed on a versioned URL to migrate user to new combined and rationalised API spec gradually.
Epic: v1 API β Multi-country Unified API
Overview
This epic covers the design and initial build of the v1 API β a unified, multi-country replacement for the existing v0 UK-only solar API. The v1 introduces a country-first URL scheme (/{country}/{source}/β¦), UUID-based region identifiers, consistent kW units throughout, and a clean separation between per-region time-series, multi-region snapshots, and a pre-warmed period matrix for large 2D data to be served without overwhelming the Data Platform or slowing down this API.
The v1 runs as an independent FastAPI sub-app mounted at /v1, with its own Scalar documentation UI, Auth0-integrated OAuth2 flow, and a config-driven cache architecture designed for future Redis migration.
Where possible, new routes are mapped 1:1 against their v0 equivalents to ensure easy-as-possible client migration.
Note
Current state: Core discovery, per-region, snapshot, and period matrix endpoints are all live with a new unit test suite. Auth passthrough to the Data Platform and the delta/accuracy family of endpoints remain outstanding.
Capabilities
Discovery
Preflight endpoints for clients to self-describe what's available before making data requests.
Route
Description
GET /v1/sources
List energy sources (solar, wind)
GET /v1/countries
List countries with full capability manifest (region types, models, generation sources)
GET /v1/{country}/{source}/region-types
Region types for a country, each with available forecast models
GET /v1/{country}/{source}/generation-sources
Observed generation data sources for a country
Note:/countries now returns a full capability manifest including region types, forecast models, and generation sources for every country. The more granular routes (/region-types, /generation-sources) predate this and overlap with it β whether they are still needed as standalone endpoints is an open question (see below). /sources is similarly simple enough to question.
Regions
Route
Description
GET /v1/{country}/{source}/regions
List all regions of a given type (?region_type=gsp), or children of a parent (?parent_id=β¦)
GET /v1/{country}/{source}/regions/{region_id}
Metadata for a single region
Forecasts
Route
Description
GET /v1/{country}/{source}/regions/{region_id}/forecast
Forecast timeseries for a single region
GET /v1/{country}/{source}/regions/{region_id}/forecast/last-updated
Creation time of the most recent forecast (cached 10s)
GET /v1/{country}/{source}/forecasts/snapshot
All regions at one timestamp (region_type required, cached 2min)
GET /v1/{country}/{source}/forecasts/period
All regions across a time window β served from pre-warmed cache
nl_regional_pv_ecmwf_mo_sat_adjust and variants, ned_nl_national
Architecture decisions
v1 is a separate FastAPI sub-app, not just extra routes
v1 is mounted as an independent FastAPI instance at /v1 via server.mount("/v1", v1_app). This gives it:
Its own OpenAPI schema at /v1/openapi.json
Its own docs UI at /v1/docs β independently configured, independently styled, free to diverge completely from v0
Its own middleware chain β rate limiting, auth, and caching all configured independently
Clean separation: v0 routes can be removed later without touching v1 code
Beyond the technical benefits, treating v1 as its own app means documentation is a first-class concern, not bolted on. We can customise the schema, branding, and interactive experience for v1 without being constrained by what v0 does. The choice to use Scalar for v1 is one example of this; deeper customisation (custom themes, curated examples, branded landing page(s)) becomes straightforward.
The trade-off is slightly more startup complexity (dependency overrides and lifespan management applied twice). This is contained in main.py.
Country-first URL structure
All v1 routes follow /{country}/{source}/β¦ rather than /forecasts/GB/solar/β¦. This mirrors the potential Auth0 permission/scopes naming convention (gb_solar_regional) and makes country the primary commercial subscription boundary β a client's token scope maps naturally to their URL prefix.
UUIDs as primary region identifiers
v1 uses UUIDs throughout for region IDs. The GET /regions?region_type=gsp endpoint returns UUID-based RegionDetail objects; clients should store the UUID and use it for all subsequent calls. A national slug alias is also accepted on per-region routes as a convenience.
v0 used integer gsp_id values. How to best help clients that still think in integers β or the official Elexon GSP ID strings β is an open question (see below).
Consistent units: kW throughout
v0 mixed MW (forecast values) and kW (PV Live observed values). v1 uses kW for everything β forecasts, observed generation, and capacity figures. Field name is always power_kW.
Snapshot and timeseries are separate routes, not a Union
/forecasts/snapshot and /forecasts/timeseries are distinct endpoints with distinct response types, distinct cache strategies, and non-overlapping use cases:
/forecasts/snapshot
/forecasts/timeseries
Shape
One time Γ N regions
N regions Γ M times
Cache
2min TTL, live DP call
Pre-warmed per-region keys, 24h TTL
Cold cache
Returns live data
Returns 503 + Retry-After: 60
Model filter
Supported
Not supported (serves pre-warmed data only)
They were not merged into one endpoint with a Union return type β the response shapes are fundamentally different and the caching requirements are incompatible.
Pre-warmed timeseries cache with per-region keys
The /forecasts/period and /generation/period endpoints serve data entirely from cache β no live DP calls per request. On startup, _warm_all_v1_caches fetches a 4-day window for every region and writes one cache key per region UUID.
Storing data per-region (rather than one large blob) means:
Sub-window queries β a request for a shorter time range or a subset of regions is resolved in-memory by filtering the cached values; no re-fetch needed
Partial updates β individual region keys can be refreshed without invalidating the whole dataset
Redis-ready β N keys can be fetched in a single pipeline round-trip when we move off InMemoryBackend
Cache warming targets are derived entirely from country config β adding a new country or observer requires only a config change, no code changes.
Config-driven country and region type definitions
All country/region/model metadata lives in service/v1/country_config.py. Each RegionTypeConfig declares:
Its user-facing type slug and label
Its internal LocationType (maps to the DP's location hierarchy)
The forecast_models available for that region type
A default_model applied when no model param is passed
A central FORECASTER_LABELS dict maps DP forecaster names to human-readable labels. This keeps the DP's internal naming out of API responses and makes label changes a one-line edit.
Scalar for v1 docs, Swagger for v0
v1 uses Scalar at /v1/docs. v0 keeps Swagger at /swagger and ReDoc at /docs. OAuth2 PKCE is configured via x-scalar-secret-client-id on the security scheme flow, with audience embedded in the authorizationUrl for Auth0 compatibility.
v0 β v1 migration map
v0 route
v1 equivalent
Notes
GET /v0/solar/GB/national/forecast
GET /v1/GB/solar/regions/national/forecast
?model=blend_adjust for trend-adjusted
GET /v0/solar/GB/national/pvlive
GET /v1/GB/solar/regions/national/generation
?observer=pvlive_in_day or pvlive_day_after
GET /v0/solar/GB/gsp/{id}/forecast
GET /v1/GB/solar/regions/{uuid}/forecast
Integer ID β UUID; see open question on ID compatibility
GET /v0/solar/GB/gsp/{id}/pvlive
GET /v1/GB/solar/regions/{uuid}/generation
GET /v0/solar/GB/gsp/forecast/all/
GET /v1/GB/solar/forecasts/snapshot?region_type=gsp
Replaces both compact and non-compact variants
POST /v0/solar/GB/gsp/forecast/all/refresh
POST /v1/GB/solar/forecasts/refresh?region_type=gsp
Warms timeseries matrix cache
GET /v0/solar/GB/gsp/pvlive/all
GET /v1/GB/solar/generation/snapshot?region_type=gsp
GET /v0/system/GB/gsp/
GET /v1/GB/solar/regions?region_type=gsp
Returns UUID-based RegionDetail
Intentionally not ported (for now)
compact param β ForecastSnapshot is compact by design
smooth_flag β not applicable for DP backend
GET /v0/solar/GB/status β separate concern, no v1 equivalent planned
Open questions / decisions still to make
1. trend_adjuster_on compatibility
v0 had a trend_adjuster_on boolean (default true) that silently swapped the underlying "model". An idea in v1 is to make this explicit: ?model=blend_adjust vs ?model=blend.
This is more explicit, but emphasises the difference, and also maybe highlights the adjuster more than we might want, poss inviting more questions...
Decisions needed:
Do we actually want clients to be able to request the unadjusted output?
Do we retain trend_adjuster boolean behaviour in v1, or adopt a parallel-model framing to keep things distinct.
2. Integer / string GSP ID compatibility
v0 clients reference regions by integer gsp_id. v1 uses UUIDs as the primary identifier. Some options for easing migration:
Accept integer gsp_id as an alias on per-region routes (look up UUID internally)
Accept official NESO GSP ID strings (e.g. ABHA1 format) as an alias
Expose gsp_id in region metadata so clients can build their own lookup once
Document the UUID lookup flow and require clients to migrate fully
3. Auth passthrough to DP
Five places in the v1 routes pass authdata={} to DP calls. Auth enforcement is currently on our side only. Needs coordination with the DP team before tightening.
4. NL generation sources and forecast models
NL has a nednl generation source configured but model availability needs confirming against live DP data.
5. CSV export
v0 had a CSV variant on the legacy regions router. Not in v1 yet. Is there a UI or downstream use case that needs it? If yes, which endpoints should support it?
6. Delta endpoints (Not for launch)
Not built yet. Three new endpoints could be added:
GET /regions/{id}/delta β per-timestep forecast vs actuals (client-side join; no DP primitive in SDK 0.25.0)
GET /regions/{id}/delta/weekly β week-average error per horizon (DP-native via GetWeekAverageDeltas, unblocked today)
GET /deltas?region_type=β¦ β snapshot of forecast-vs-actual across all regions
GET /regions/{id}/delta/horizons β compare two forecast horizons
7. Discovery route redundancy /countries now returns a full capability manifest β region types, forecast models, and generation sources β making the more granular /region-types and /generation-sources routes largely redundant for any client that calls /countries first. /sources is similarly thin. Options:
Remove /region-types, /generation-sources, and /sources and point clients to /countries
Keep them as convenience endpoints for targeted queries (avoids parsing the full countries payload)
Retain for now, revisit once we have real client feedback on usage patterns
8. Wind source GET /sources already returns wind but no country has wind region types configured. Placeholder for when DP wind data is available.
9. Archival / Historic data
Particularly for regional-level data but also National, what limit backwards should users be able to get?
How large a date range in a single query?
What rate-limiting on these sorts of requests?
Epic: v1 API β Multi-country Unified API
Overview
This epic covers the design and initial build of the v1 API β a unified, multi-country replacement for the existing v0 UK-only solar API. The v1 introduces a country-first URL scheme (
/{country}/{source}/β¦), UUID-based region identifiers, consistent kW units throughout, and a clean separation between per-region time-series, multi-region snapshots, and a pre-warmed period matrix for large 2D data to be served without overwhelming the Data Platform or slowing down this API.The v1 runs as an independent FastAPI sub-app mounted at
/v1, with its own Scalar documentation UI, Auth0-integrated OAuth2 flow, and a config-driven cache architecture designed for future Redis migration.Where possible, new routes are mapped 1:1 against their v0 equivalents to ensure easy-as-possible client migration.
Note
Current state: Core discovery, per-region, snapshot, and period matrix endpoints are all live with a new unit test suite. Auth passthrough to the Data Platform and the delta/accuracy family of endpoints remain outstanding.
Capabilities
Discovery
Preflight endpoints for clients to self-describe what's available before making data requests.
GET /v1/sourcessolar,wind)GET /v1/countriesGET /v1/{country}/{source}/region-typesGET /v1/{country}/{source}/generation-sourcesRegions
GET /v1/{country}/{source}/regions?region_type=gsp), or children of a parent (?parent_id=β¦)GET /v1/{country}/{source}/regions/{region_id}Forecasts
GET /v1/{country}/{source}/regions/{region_id}/forecastGET /v1/{country}/{source}/regions/{region_id}/forecast/last-updatedGET /v1/{country}/{source}/forecasts/snapshotregion_typerequired, cached 2min)GET /v1/{country}/{source}/forecasts/periodPOST /v1/{country}/{source}/forecasts/refreshocf:admin)Generation (observed)
GET /v1/{country}/{source}/regions/{region_id}/generationGET /v1/{country}/{source}/generation/snapshotregion_typerequired, cached 2min)GET /v1/{country}/{source}/generation/periodPOST /v1/{country}/{source}/generation/refreshocf:admin)Countries currently configured
GBnational,gsp,dnoNLnational,netbeheerderArchitecture decisions
v1 is a separate FastAPI sub-app, not just extra routes
v1 is mounted as an independent
FastAPIinstance at/v1viaserver.mount("/v1", v1_app). This gives it:/v1/openapi.json/v1/docsβ independently configured, independently styled, free to diverge completely from v0Beyond the technical benefits, treating v1 as its own app means documentation is a first-class concern, not bolted on. We can customise the schema, branding, and interactive experience for v1 without being constrained by what v0 does. The choice to use Scalar for v1 is one example of this; deeper customisation (custom themes, curated examples, branded landing page(s)) becomes straightforward.
The trade-off is slightly more startup complexity (dependency overrides and lifespan management applied twice). This is contained in
main.py.Country-first URL structure
All v1 routes follow
/{country}/{source}/β¦rather than/forecasts/GB/solar/β¦. This mirrors the potential Auth0 permission/scopes naming convention (gb_solar_regional) and makes country the primary commercial subscription boundary β a client's token scope maps naturally to their URL prefix.UUIDs as primary region identifiers
v1 uses UUIDs throughout for region IDs. The
GET /regions?region_type=gspendpoint returns UUID-basedRegionDetailobjects; clients should store the UUID and use it for all subsequent calls. Anationalslug alias is also accepted on per-region routes as a convenience.v0 used integer
gsp_idvalues. How to best help clients that still think in integers β or the official Elexon GSP ID strings β is an open question (see below).Consistent units: kW throughout
v0 mixed MW (forecast values) and kW (PV Live observed values). v1 uses kW for everything β forecasts, observed generation, and capacity figures. Field name is always
power_kW.Snapshot and timeseries are separate routes, not a Union
/forecasts/snapshotand/forecasts/timeseriesare distinct endpoints with distinct response types, distinct cache strategies, and non-overlapping use cases:/forecasts/snapshot/forecasts/timeseriesRetry-After: 60They were not merged into one endpoint with a Union return type β the response shapes are fundamentally different and the caching requirements are incompatible.
Pre-warmed timeseries cache with per-region keys
The
/forecasts/periodand/generation/periodendpoints serve data entirely from cache β no live DP calls per request. On startup,_warm_all_v1_cachesfetches a 4-day window for every region and writes one cache key per region UUID.Storing data per-region (rather than one large blob) means:
InMemoryBackendCache warming targets are derived entirely from country config β adding a new country or observer requires only a config change, no code changes.
Config-driven country and region type definitions
All country/region/model metadata lives in
service/v1/country_config.py. EachRegionTypeConfigdeclares:typeslug andlabelLocationType(maps to the DP's location hierarchy)forecast_modelsavailable for that region typedefault_modelapplied when nomodelparam is passedA central
FORECASTER_LABELSdict maps DP forecaster names to human-readable labels. This keeps the DP's internal naming out of API responses and makes label changes a one-line edit.Scalar for v1 docs, Swagger for v0
v1 uses Scalar at
/v1/docs. v0 keeps Swagger at/swaggerand ReDoc at/docs. OAuth2 PKCE is configured viax-scalar-secret-client-idon the security scheme flow, withaudienceembedded in theauthorizationUrlfor Auth0 compatibility.v0 β v1 migration map
GET /v0/solar/GB/national/forecastGET /v1/GB/solar/regions/national/forecast?model=blend_adjustfor trend-adjustedGET /v0/solar/GB/national/pvliveGET /v1/GB/solar/regions/national/generation?observer=pvlive_in_dayorpvlive_day_afterGET /v0/solar/GB/gsp/{id}/forecastGET /v1/GB/solar/regions/{uuid}/forecastGET /v0/solar/GB/gsp/{id}/pvliveGET /v1/GB/solar/regions/{uuid}/generationGET /v0/solar/GB/gsp/forecast/all/GET /v1/GB/solar/forecasts/snapshot?region_type=gspPOST /v0/solar/GB/gsp/forecast/all/refreshPOST /v1/GB/solar/forecasts/refresh?region_type=gspGET /v0/solar/GB/gsp/pvlive/allGET /v1/GB/solar/generation/snapshot?region_type=gspGET /v0/system/GB/gsp/GET /v1/GB/solar/regions?region_type=gspRegionDetailIntentionally not ported (for now)
compactparam βForecastSnapshotis compact by designsmooth_flagβ not applicable for DP backendGET /v0/solar/GB/statusβ separate concern, no v1 equivalent plannedOpen questions / decisions still to make
1.
trend_adjuster_oncompatibilityv0 had a
trend_adjuster_onboolean (defaulttrue) that silently swapped the underlying "model". An idea in v1 is to make this explicit:?model=blend_adjustvs?model=blend.This is more explicit, but emphasises the difference, and also maybe highlights the adjuster more than we might want, poss inviting more questions...
trend_adjusterboolean behaviour in v1, or adopt a parallel-model framing to keep things distinct.2. Integer / string GSP ID compatibility
v0 clients reference regions by integer
gsp_id. v1 uses UUIDs as the primary identifier. Some options for easing migration:gsp_idas an alias on per-region routes (look up UUID internally)ABHA1format) as an aliasgsp_idin region metadata so clients can build their own lookup once3. Auth passthrough to DP
Five places in the v1 routes pass
authdata={}to DP calls. Auth enforcement is currently on our side only. Needs coordination with the DP team before tightening.4. NL generation sources and forecast models
NL has a
nednlgeneration source configured but model availability needs confirming against live DP data.5. CSV export
v0 had a CSV variant on the legacy regions router. Not in v1 yet. Is there a UI or downstream use case that needs it? If yes, which endpoints should support it?
6. Delta endpoints (Not for launch)
Not built yet. Three new endpoints could be added:
GET /regions/{id}/deltaβ per-timestep forecast vs actuals (client-side join; no DP primitive in SDK 0.25.0)GET /regions/{id}/delta/weeklyβ week-average error per horizon (DP-native viaGetWeekAverageDeltas, unblocked today)GET /deltas?region_type=β¦β snapshot of forecast-vs-actual across all regionsGET /regions/{id}/delta/horizonsβ compare two forecast horizons7. Discovery route redundancy
/countriesnow returns a full capability manifest β region types, forecast models, and generation sources β making the more granular/region-typesand/generation-sourcesroutes largely redundant for any client that calls/countriesfirst./sourcesis similarly thin. Options:/region-types,/generation-sources, and/sourcesand point clients to/countries8. Wind source
GET /sourcesalready returnswindbut no country has wind region types configured. Placeholder for when DP wind data is available.9. Archival / Historic data
Particularly for regional-level data but also National, what limit backwards should users be able to get?
How large a date range in a single query?
What rate-limiting on these sorts of requests?