Health
as-of view
API keys
Access requests
approve creates a read key (btd+entsoe by default) — you send it to them yourselfCreate a key
the full key is shown once, then never againKeys
click a key to see its requests; edit source access inline; revocation is immediateDownload
Quickstart
Everything is keyed. Pass the key in the X-API-Key header
(or ?api_key= for SSE, where headers aren't possible).
# who am I — identity, entitlements, limits, today's usage
curl -H "X-API-Key: $LFL_KEY" {ORIGIN}/v1/me
# discover series (AI-first metadata; filter with ?q= ?category= ?zone= ?source=)
curl -H "X-API-Key: $LFL_KEY" "{ORIGIN}/v1/catalog?q=imbalance"
# fetch data (UTC ISO8601; timestamps label period START; end is exclusive)
curl -H "X-API-Key: $LFL_KEY" \
"{ORIGIN}/v1/series/imbalance_price/data?zone=EE&start=2026-07-10T00:00:00Z&end=2026-07-11T00:00:00Z"
The machine-readable OpenAPI schema lives at {ORIGIN}/openapi.json —
keyed like everything else, so fetch it with the X-API-Key header.
Concepts
- Bi-temporal storage. Every value carries
valid_time(the delivery interval it describes) andknowledge_time(when it became knowable). Revisions are archived, never overwritten. Passas_of=<ts>to any data query to reconstruct exactly what was knowable at that instant. - Forecast vintages. Forecasts keep every run (
issued_at). Default = best-known value per period;issue_time=latestreturns the newest coherent curve; an exact timestamp returns that vintage. List runs via/v1/series/{id}/runs. - Dimensions.
zone(EE/LV/LT/…) andentity(corridor, station, fuel) are query parameters, never part of the series id. Direction and model family are part of the id (afrr_lmp_up,wind_forecast_ecmwf). - Sources. A series can be carried by several upstreams (e.g. BTD primary, ENTSO-E secondary). You get the first source your key is entitled to.
- Storage shapes. Scalar observations, forecasts, native 4-second ticks
(capped at 2 days/request), and bid stacks (query
/bids, not/data). - Delta pulls. Pass
since=<watermark>to/dataor/bidsto receive only values that became knowable after it — new points and revisions of old periods in one response. Poll with your lastknowledge_timeinstead of re-downloading windows; composes withas_of(since < knowledge_time ≤ as_of).
REST API
| endpoint | what it returns |
|---|---|
| GET /v1/catalog | Series list with unit, zones, resolution, sources. Filters: q, category, zone, source, status |
| GET /v1/catalog/{id} | Full metadata: description, quirks, publication lag, usage hints, entities |
| GET /v1/catalog/categories | The category taxonomy tree |
| GET /v1/series/{id}/data | Values in [start, end). Params: zone, entity, source, as_of, since, issue_time, format=json|csv |
| GET /v1/series/{id}/latest | Newest value + staleness for one zone/entity |
| GET /v1/series/{id}/runs | Forecast vintages, newest first |
| GET /v1/export | Bulk file download: series (any number), start/end, zones, tz (IANA), format=csv|xlsx. Excel = one sheet per series, a column per zone |
| GET /v1/series/{id}/bids | Bid-stack rows (price, quantity, direction, merit position) per MTU; since= returns only stacks published after your watermark |
| GET /v1/series/{id}/validity | Per-day integrity audit: stored vs expected counts, revisions, knowledge lag |
| GET /v1/jobs/{id}/runs | Run history of one ingest job (window, rows changed, errors) |
| GET /v1/stream | SSE push of change events (?api_key= auth) — updates carry an event id; reconnects replay the gap via Last-Event-ID |
| GET /v1/me | Your key: scopes, source entitlements, limits, usage today |
| GET /v1/me/usage | Per-day usage history for your key (?days=30, max 90) — newest first |
| /v1/admin/keys[/{id}] | Key management (admin scope): GET list w/ usage, POST create, PATCH access/limits, DELETE revoke (+?purge=true deletes a revoked key for good) |
| /v1/admin/keys/{id}/requests | Per-key request log (admin scope): every data request with series, zones, window and outcome, kept 60 days — errors=true shows only denials and throttles, before=<ts> pages back |
| PATCH /v1/admin/series/{id} | Set or clear the expected-arrival override (admin) — the schedule the catalog, API and MCP report for model building |
| GET /v1/status, /freshness, /sources, /storage | Pipeline health: expectation vs reality, per-series freshness, what is physically stored |
| GET /llms.txt | The whole platform as one plain-text file for AI agents — concepts, endpoints, and every series with its upstream-id mappings. Always generated live |
| GET /v1/health, /v1/summary | Public: liveness + counts-only platform summary |
| POST /v1/access-request | Public: ask the operator for a key (name, contact, use case) — keys are never self-service |
Range caps: 92 days per request (2 days for tick series, 7 for bid stacks). Split longer
ranges into chunks. All error detail strings say what to do instead.
# point-in-time (backtest-grade): the day-ahead curve as it was knowable at 09:00
curl -H "X-API-Key: $LFL_KEY" \
"{ORIGIN}/v1/series/day_ahead_price/data?zone=EE&start=2026-07-10T00:00:00Z&end=2026-07-11T00:00:00Z&as_of=2026-07-10T09:00:00Z"
# one coherent forecast run instead of the best-known splice
curl -H "X-API-Key: $LFL_KEY" \
"{ORIGIN}/v1/series/wind_forecast_ecmwf/data?zone=LV&start=2026-07-11T00:00:00Z&end=2026-07-13T00:00:00Z&issue_time=latest"
# delta pull: only what became knowable after my watermark (new points AND revisions)
curl -H "X-API-Key: $LFL_KEY" \
"{ORIGIN}/v1/series/imbalance_price/data?zone=EE&start=2026-07-10T00:00:00Z&end=2026-07-11T00:00:00Z&since=2026-07-10T12:00:00Z"
Series reference
Every series id the API serves, with what it is and when new data
arrives. Click a row to open the series. Full metadata (quirks, usage
hints, entities) via GET /v1/catalog/{id}.
Live push (SSE)
One long-lived connection; update events fire the moment ingestion writes a
value that actually changed — observations, forecasts, ticks and bid stacks alike.
Payload lists the affected series+zones — fetch fresh values via /data
(pass since= for just the delta) or /latest. Events are named:
use addEventListener, not onmessage.
Every update carries an event id. When the connection drops, EventSource reconnects
with Last-Event-ID automatically and the missed events (up to ~24 h) are
replayed in order — nothing is lost in the gap. A reset event means the gap
could not be proven contiguous: do one full refresh, then continue live. That
pull-after-update loop with a since= watermark is the intended terminal
integration.
const es = new EventSource("{ORIGIN}/v1/stream?api_key=" + KEY);
es.addEventListener("update", (ev) => {
const { series, max_valid_time } = JSON.parse(ev.data);
// ev.lastEventId = replay cursor (sent back automatically on reconnect)
// [{series_key: "imbalance_price", zone: "EE"}, ...]
});
es.addEventListener("reset", () => {/* replay gap too old — refetch in full */});
es.addEventListener("heartbeat", () => {/* every 30 s, keeps proxies open */});
Connect an AI client (MCP)
The same data over the Model Context Protocol — 7 tools (catalog search, series
description, data with auto-downsampling, latest values, forecast runs, source health).
Values come back as compact CSV-in-text, resampled to ≤300 points. Pick your client
below and swap YOUR_KEY for the key you were issued. The agent discovers
series and semantics on its own, so you can just ask
"compare the latest ECMWF vs GFS wind forecast for LV".
Claude Desktop — easiest for most people. Needs
Node.js installed. Add this to
claude_desktop_config.json (macOS:
~/Library/Application Support/Claude/, Windows:
%APPDATA%\Claude\), then fully quit and reopen the app:
{
"mcpServers": {
"lowerflower": {
"command": "npx",
"args": ["-y", "mcp-remote", "{ORIGIN}/mcp/?api_key=YOUR_KEY"]
}
}
}
claude.ai (web) — no install, needs a paid plan. Settings → Connectors → Add custom connector, set Authentication: None, and paste this URL (the key rides in the URL, so leave auth off):
{ORIGIN}/mcp/?api_key=YOUR_KEY
Claude Code — the terminal CLI, uses a real header:
claude mcp add -s user --transport http lowerflower {ORIGIN}/mcp/ \
--header "X-API-Key: YOUR_KEY"
Self-hosted / local checkout — stdio, no key needed (talks straight to your DB):
claude mcp add lowerflower -- uv run --directory ~/Desktop/E2E_LFL lfl mcp
One-shot context (any model, no MCP) — the whole platform as a single plain-text file: concepts, every endpoint, and every series with its upstream-id mappings. Fetch it once and paste or attach it:
curl -H "X-API-Key: $LFL_KEY" {ORIGIN}/llms.txt > lfl-context.txt
Access, limits & licensing
- Keys.
lfl_<key_id>_<secret>; the secret is stored hashed and shown once at creation. Keys are issued by the operator only — request one from the launch page ("Request access") and you will hear back at the contact you give. Lifecycle via CLI:lfl keys create --name alice [--sources btd,entsoe] [--expires-days 90],lfl keys list,lfl keys revoke <key_id>. Revocation is immediate. - Source entitlements. Each key may be restricted to specific upstreams.
Syspower (Volue/SKM) data is contractually licensed — it is only granted to keys
whose owners hold their own license. Unentitled requests get a 403 with the reason;
multi-source series transparently fall back to a source you may read; the SSE stream
only shows you events for series you can access. Check yours at
/v1/me. - Rate limit. 600 requests/minute per key (per-key override possible). On 429,
honor
Retry-After. - Metering. Requests and points served are recorded per key per day —
visible in
/v1/measusage_today. - Scopes.
readfor data;adminunlocks the Admin console and the/v1/admin/keysendpoints — create keys, edit source access, change rate limits, revoke. Every mutation is written to the audit log.
Errors
All errors are RFC 9457 application/problem+json with a detail
written for self-correction:
{"type": "about:blank", "title": "Not entitled", "status": 403,
"detail": "This key is not entitled to source(s) ['syspower'] ... contact the operator."}
| status | meaning |
|---|---|
| 401 | No key presented |
| 403 | Key unknown/revoked/expired, or not entitled to this source |
| 404 | Unknown series id, or no data yet |
| 422 | Bad parameters — the detail names the parameter and the fix |
| 429 | Rate limited — wait Retry-After seconds |