@uk-energy/elexon
v0.3.0
Published
Typed client for Elexon Insights (BMRS): bid-offer acceptances, bid-offer data, physical notifications, generation by fuel, BMU reference, system prices. Part of uk-energy-sdk.
Maintainers
Readme
@uk-energy/elexon
Typed client for Elexon Insights (BMRS): bid-offer acceptances and prices, physical notifications, generation by fuel, BM unit reference data and system prices. No API key. Types are generated from Elexon's OpenAPI document, so field names and nullability are the API's own.
Contains BMRS data © Elexon Limited copyright and database right 2026. Keep this line wherever you show the data. The BMRS data licence allows copying, adapting and selling derived works; it does not imply endorsement.
Python mirror: uk_energy.elexon in the uk-energy distribution.
pnpm add @uk-energy/elexonUsage
import { ElexonClient } from "@uk-energy/elexon";
const ex = new ElexonClient();
// Bid-offer acceptances for one hour, every unit. soFlag=true bids on wind units are curtailment.
const boalf = await ex.datasets.boalf({ from: "2026-10-15T00:00Z", to: "2026-10-15T01:00Z" });
const curtailed = boalf.filter((r) => r.soFlag && r.bmUnit?.endsWith("W-1"));
// Or by settlement period: from/to are then dates, not instants.
await ex.datasets.boalf({
from: "2026-10-15", to: "2026-10-15",
settlementPeriodFrom: 1, settlementPeriodTo: 4,
bmUnit: ["T_ARCHW-1"],
});
// Submitted bid/offer prices (BOD). Wind units bid negative: they are paid to turn down.
const bod = await ex.datasets.bod({ from: "2026-10-15T00:00Z", to: "2026-10-15T01:00Z", bmUnit: ["T_ARCHW-1"] });
// Generation by fuel type, 5-minute resolution
const fuel = await ex.datasets.fuelinst({ publishDateTimeFrom: "2026-10-15T00:00Z", publishDateTimeTo: "2026-10-15T00:30Z" });
// Every BM unit, with fuel type and capacity
const units = await ex.reference.bmUnits();
const wind = units.filter((u) => u.fuelType === "WIND"); // 287 on 20 Oct 2026
// System prices for a settlement day or one period
const prices = await ex.balancing.systemPrices("2026-10-15"); // 48 rows
const sp1 = await ex.balancing.systemPrices("2026-10-15", 1);
// Any other /datasets/{code}/stream endpoint, typed by code
const mels = await ex.datasets.stream("MELS", { from: "2026-10-15T00:00Z", to: "2026-10-15T01:00Z" });Curtailment
// Every WIND unit (287), one settlement day: the reference list + 3 requests per 200 units (BOAV, EBOCF, BOALF)
const day = await ex.curtailment.forDay("2026-10-15");
day.total; // { curtailedMwh, soFlaggedMwh, cashflowGbp, units, acceptances }
day.units[0]; // biggest curtailed unit: { bmUnit, leadPartyName, gspGroupId, capacityMw, curtailedMwh, cashflowGbp, … }
day.periods; // one row per (unit, settlement period) with volume, £ and SO flag counts
// A few units, a few periods
await ex.curtailment.forDay("2026-10-15", { bmUnit: ["T_AKGLW-3"], settlementPeriodFrom: 1, settlementPeriodTo: 8 });
// The feeds behind it
await ex.balancing.acceptanceVolumes("bid", "2026-10-15", { bmUnit: ["T_AKGLW-3"] }); // BOAV, MWh per acceptance per period
await ex.balancing.cashflows("bid", "2026-10-15", { bmUnit: ["T_AKGLW-3"] }); // EBOCF, £ per unit per period
await ex.reference.units({ fuelType: ["WIND"], gspGroupId: ["_P", "_N"] }); // Scottish wind unitsOctopus's "Wasted Wind" figure
const day = await ex.curtailment.wastedWind("2026-09-30");
day.total; // { curtailedMwh: 124226, curtailmentCostGbp: 4040737, turnUpMwh, turnUpCostGbp: 25986249, totalCostGbp: 30026986 }
day.periods[19]; // SP20 (09:30 BST): 2,415.70 MWh, £72,256.84 to switch off, £506,067.61 to replacewastedwind.energy's definition, reproduced from the settlement bid
and offer stacks (balancing.settlementStack): SO-flagged wind bids priced at their bid price,
plus the curtailed volume re-bought from the cheapest non-SO offers in the same period. Two
requests per settlement period. It is a definition, not settlement truth — see
examples/ for a month reproduced to 0.2% and a comparison with the settled
view below.
A curtailment is a bid acceptance: NESO accepts the unit's (negative-priced) bid to reduce
output, so the unit is paid to turn down and cashflowGbp is positive. soFlaggedMwh is the
share from acceptances BOALF marks soFlag: true — system (constraint) actions rather than energy
balancing. On 15 Oct 2026, AKGLW-3 was curtailed in 42 of 48 periods for 1,019 MWh and £49.5k,
all SO-flagged. aggregateCurtailment() is exported for callers with their own rows.
Every method is async and returns the API's rows unwrapped: /stream endpoints are bare arrays,
/datasets/{code} and /balancing/* endpoints come back as { data, metadata } and only data
is returned. A response of the wrong shape throws UkEnergyError; an HTTP error throws
HttpError with the API's RFC 9110 problem details in body.
Surface
| Namespace | Methods |
| --- | --- |
| datasets | stream(code, query), boalf, bod, fuelinst, pn(date, sp, filter), pnStream, mels, mils |
| reference | bmUnits(), fuelTypes(), units({ fuelType?, gspGroupId?, leadParty? }) |
| balancing | systemPrices(date, sp?), marketIndex, acceptances(date, sp, filter), bidOffers(date, sp, filter), physical(dataset, date, sp, filter), acceptanceVolumes(bidOffer, date, filter), cashflows(bidOffer, date, filter), settlementStack(bidOffer, date, sp) |
| curtailment | forDay(date, { bmUnit?, fuelType?, settlementPeriodFrom?, settlementPeriodTo? }), wastedWind(date, { settlementPeriodFrom?, settlementPeriodTo?, fuelType?, concurrency? }) |
Row types are exported: BoalfRow, BodRow, FuelinstRow, PnRow, MelsRow, BmUnit,
SystemPriceRow, MarketIndexRow, AcceptanceRow, BidOfferRow, PhysicalRow, plus the
SettlementWindow / UnitFilter / FuelinstQuery argument types; the full
generated paths/components are under @uk-energy/elexon as well for anything not wrapped.
Things to know about the API
- Settlement time is local. A settlement day starts at Europe/London midnight; in BST,
SP1 of 15 Oct starts at
2026-10-14T23:00Z. Days have 48 periods, 46 on the spring clock change and 50 on the autumn one.@uk-energy/coreexportssettlementPeriodAt,settlementPeriodStartand friends for the conversion. from/tochange meaning on BOALF/BOD: instants normally, but calendar dates whensettlementPeriodFrom/Toare given.- BOALF is a volume of levels, not a list of curtailment events. One hour across all
units is ~1,650 rows; one settlement period of acceptances is ~730. Filter by
bmUnitwhere you can. reference/bmunits/allis ~3,100 units with capacities as strings ("114.000"), andgspGroupIdisnullfor 235 of the 287 wind units (transmission-connected), so GSP group cannot place most wind farms on a constraint boundary. 48 wind units have noelexonBmUnit;unitId()falls back tonationalGridBmUnit, which the API accepts as a filter too. The list also has duplicates (T_WLNYO-4twice);curtailment.forDayde-duplicates ids.- Unit filters can be long. The API accepted all 287 wind ids in one 4.9 KB URL; the client
splits lists at
MAX_UNITS_PER_REQUEST(200) anyway. Unfiltered daily BOAV is ~9 MB. - Rate limits: none published and none observed (310 requests at ~120 rps on 20 Oct 2026,
all 200). The client defaults to 10 rps / burst 20 per host, shared across clients in the
process; pass
rateLimitto change it. - Errors are RFC 9110 problem details with a
400for afromlater thantoor a bad dataset code.
Options
new ElexonClient({
rateLimit: { requestsPerSecond: 10, burst: 20 }, // default
cache: new MemoryCache(), // optional; TTL defaults to 5 minutes
cacheTtlMs: 5 * 60 * 1000,
retry: { retries: 3 }, timeoutMs: 30_000, fetch, // forwarded to @uk-energy/core HttpClient
});Regenerating the types
openapi/insights-v1.json is a snapshot of https://data.elexon.co.uk/swagger/v1/swagger.json.
Replace it and run pnpm --filter @uk-energy/elexon generate (TypeScript) and
uv run python scripts/gen_elexon_types.py from python/ (Python TypedDicts).
