Building a Type-Safe SDK for Energy API: Patterns for Client Libraries, Versioning, and Offline Replay in TypeScript and Rust
You have energy market features to ship—alerts, cost simulators, carbon dashboards—but the data is scattered across OMIE, ENTSO-E, EIA/FRED, ESIOS, and more. By the end of this post, you’ll know how to design a type-safe SDK for Energy API in both TypeScript and Rust, version it cleanly, cache and replay requests when offline, and wire up a few high-value use cases without wrestling with inconsistent schemas or brittle scrapers.
Introduction
Most teams underestimate the time it takes to normalize energy market data. Electricity hourly curves, gas day-ahead prices, oil OHLC, emissions allowances, and carbon intensity all come from different portals with different refresh schedules and formats. ETL scripts multiply, symbols diverge, and a small integration quietly turns into a maintenance project.
Energy API consolidates these sources behind a single REST interface with one JSON schema across commodities. That means the SDK work you do once for gas also applies to electricity, oil, coal, carbon, and grid carbon intensity. With a strongly typed client, you can preempt integration drift, reduce runtime surprises, and keep your analytics and trading tools stable even as you add symbols and endpoints.
This post covers patterns for a production-grade SDK: type safety in TypeScript and Rust, backwards-compatible versioning, request shaping and validation, multi-commodity calls, and an offline replay queue for resilience. We’ll also walk through four core endpoints you’ll use daily and share cut-and-paste examples.
Why Energy API
Energy developers don’t just need “data”—they need predictable, composable building blocks. Here are concrete benefits you get when building on Energy API:
- Unified schema across commodities: Query TTF gas, Brent crude, EU ETS allowances, and electricity in one call. Your SDK parses one shape instead of branching on data source quirks.
- Normalized symbols and metadata: Discoverable symbols with categories, currencies, and frequencies let you validate requests at compile-time (TypeScript/Rust enums) and at runtime.
- Intraday electricity where available: Hourly/15-min curves surface through category endpoints for time-of-use analytics, forecasting pipelines, and retail price references.
- Deterministic historical and latest semantics: Endpoints define behavior for non-publishing days and include currency/frequency context, keeping your charts and P&L views stable.
Quick Start
Base URL:
https://energy-api.com/api/v1
Authentication:
Pass your key via the api_key query parameter, e.g. ?api_key=YOUR_API_KEY.
First request: fetch the most recent prices for Brent crude, TTF gas, and EU ETS allowances in a single call.
curl -G https://energy-api.com/api/v1/latest \
--data-urlencode "symbols=BRENT_CRUDE,TTF_GAS,EUA_CO2" \
--data-urlencode "api_key=YOUR_API_KEY"
Illustrative JSON response:
{
"success": true,
"date": "2026-06-11",
"base": "MIXED",
"rates": {
"BRENT_CRUDE": 74.82,
"TTF_GAS": 38.15,
"EUA_CO2": 67.40
},
"dates": {
"BRENT_CRUDE": "2026-06-11",
"TTF_GAS": "2026-06-11",
"EUA_CO2": "2026-06-11"
},
"currencies": {
"BRENT_CRUDE": "USD",
"TTF_GAS": "EUR",
"EUA_CO2": "EUR"
}
}
Key fields you’ll wire into your app:
- date: ISO date of the snapshot (note: each symbol’s last published date may vary, see dates map).
- rates: last price per symbol.
- currencies: per-symbol currency code (e.g., USD, EUR). Do not assume a global base.
- dates: per-symbol date to avoid mixing stale and fresh prints.
Core Endpoints
Below are four endpoints you’ll use to build a robust energy data workflow. Each example is copy-paste ready.
1) Discover Symbols — GET /symbols
Use this to power autocomplete, validation, or SDK enums at build time. You can filter by category to scope your UI or dataset.
Key params:
- category (optional): gas | electricity | oil | coal | carbon_intensity
- base (optional): currency filter
- provider (optional)
curl -G https://energy-api.com/api/v1/symbols \
--data-urlencode "category=gas" \
--data-urlencode "api_key=YOUR_API_KEY"
{
"success": true,
"count": 3,
"symbols": [
{
"symbol": "TTF_GAS",
"name": "TTF Natural Gas Day-Ahead",
"category": "gas",
"country_code": "EU",
"currency_code": "EUR",
"frequency": "daily",
"description": "TTF day-ahead price published by EEX."
}
]
}
Field notes: Build your TypeScript or Rust enums from symbol and category. Use currency_code and frequency to annotate charts and rollups without hard-coding assumptions.
2) Latest Prices — GET /latest
Grab present-state signals for trading UI, dashboards, or alerts. You can query multiple commodities in one call, a key SDK advantage.
Key params:
- symbols (required): comma-separated (e.g., BRENT_CRUDE,TTF_GAS,EUA_CO2)
- base (optional): filter by currency if needed
- category (optional)
curl -G https://energy-api.com/api/v1/latest \
--data-urlencode "symbols=BRENT_CRUDE,TTF_GAS,EUA_CO2" \
--data-urlencode "api_key=YOUR_API_KEY"
{
"success": true,
"date": "2026-06-11",
"base": "MIXED",
"rates": {
"BRENT_CRUDE": 74.82,
"TTF_GAS": 38.15,
"EUA_CO2": 67.40
},
"dates": {
"BRENT_CRUDE": "2026-06-11",
"TTF_GAS": "2026-06-11",
"EUA_CO2": "2026-06-11"
},
"currencies": {
"BRENT_CRUDE": "USD",
"TTF_GAS": "EUR",
"EUA_CO2": "EUR"
}
}
Tip: Treat base = "MIXED" as a signal to display per-symbol currencies in your UI or convert downstream.
3) One-Day Historical Snapshot — GET /historical
Useful for point-in-time backtests and reconciliation reports. If the given date is a non-publishing day, you’ll get the most recent value before it.
Key params:
- date (required): YYYY-MM-DD
- symbols (required): comma-separated
- base (optional)
curl -G https://energy-api.com/api/v1/historical \
--data-urlencode "date=2025-09-15" \
--data-urlencode "symbols=BRENT_CRUDE,TTF_GAS" \
--data-urlencode "api_key=YOUR_API_KEY"
{
"success": true,
"date": "2025-09-15",
"base": "MIXED",
"rates": {
"BRENT_CRUDE": 71.45,
"TTF_GAS": 36.20
},
"currencies": {
"BRENT_CRUDE": "USD",
"TTF_GAS": "EUR"
}
}
Note: Your SDK should document that “date” is the requested anchor date. The values returned may reflect the last available publication before that date.
4) Timeseries — GET /timeseries
For charts, rolling windows, risk metrics, and ML features, the timeseries endpoint returns symbol-keyed date maps.
Key params:
- start (required): YYYY-MM-DD
- end (required): YYYY-MM-DD
- symbols (required)
- base (optional)
curl -G https://energy-api.com/api/v1/timeseries \
--data-urlencode "start=2025-01-01" \
--data-urlencode "end=2025-03-31" \
--data-urlencode "symbols=BRENT_CRUDE,TTF_GAS" \
--data-urlencode "api_key=YOUR_API_KEY"
{
"success": true,
"base": "MIXED",
"start_date": "2025-01-01",
"end_date": "2025-03-31",
"rates": {
"BRENT_CRUDE": {
"2025-01-02": 76.30,
"2025-01-03": 75.90
},
"TTF_GAS": {
"2025-01-02": 46.80,
"2025-01-03": 47.10
}
},
"frequencies": {
"BRENT_CRUDE": "daily",
"TTF_GAS": "daily"
},
"currencies": {
"BRENT_CRUDE": "USD",
"TTF_GAS": "EUR"
}
}
Interpretation: rates is a symbol-to-date-to-value map. Use frequencies to label visualizations and currencies to prevent unit mistakes.
Type-Safe SDK Patterns (TypeScript and Rust)
A good SDK keeps your application logic simple and correct. Below are patterns to reduce bugs and ease upgrades.
Shared design goals
- Strong types for symbols, categories, and response maps.
- Composable query builders with compile-time enforcement of required params.
- Runtime validation of JSON shapes where feasible.
- Portable retry/backoff and offline replay for transient failures.
- Non-breaking SDK releases track the API version (v1) and add new fields without breaking existing types.
TypeScript: types, client, and guards
// Types for core entities
export type Category = "gas" | "electricity" | "oil" | "coal" | "carbon_intensity";
export interface SymbolsResponse {
success: true;
count: number;
symbols: Array<{
symbol: string;
name: string;
category: Category;
country_code: string;
currency_code: string;
frequency: string;
description?: string;
}>;
}
export interface LatestResponse {
success: true;
date: string;
base: string; // often "MIXED"
rates: Record<string, number>;
dates: Record<string, string>;
currencies: Record<string, string>;
}
export interface HistoricalResponse {
success: true;
date: string; // requested anchor date
base: string;
rates: Record<string, number>;
currencies: Record<string, string>;
}
export interface TimeseriesResponse {
success: true;
base: string;
start_date: string;
end_date: string;
rates: Record<string, Record<string, number>>;
frequencies: Record<string, string>;
currencies: Record<string, string>;
}
// Minimal client
export class EnergyApiClient {
constructor(private apiKey: string, private baseUrl = "https://energy-api.com/api/v1") {}
private async get<T>(path: string, params: Record<string, string>): Promise<T> {
const url = new URL(path, this.baseUrl);
Object.entries({ ...params, api_key: this.apiKey }).forEach(([k, v]) => {
url.searchParams.set(k, v);
});
const res = await fetch(url.toString());
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error || `HTTP ${res.status}`);
}
return (await res.json()) as T;
}
symbols(filters: Partial<{ category: Category; base: string; provider: string }> = {}) {
const params: Record<string, string> = {};
if (filters.category) params.category = filters.category;
if (filters.base) params.base = filters.base;
if (filters.provider) params.provider = filters.provider;
return this.get<SymbolsResponse>("/symbols", params);
}
latest(symbols: string[]) {
if (!symbols.length) throw new Error("symbols required");
return this.get<LatestResponse>("/latest", { symbols: symbols.join(",") });
}
historical(date: string, symbols: string[]) {
if (!date) throw new Error("date required");
if (!symbols.length) throw new Error("symbols required");
return this.get<HistoricalResponse>("/historical", {
date,
symbols: symbols.join(","),
});
}
timeseries(start: string, end: string, symbols: string[]) {
if (!start || !end) throw new Error("start and end required");
if (!symbols.length) throw new Error("symbols required");
return this.get<TimeseriesResponse>("/timeseries", {
start,
end,
symbols: symbols.join(","),
});
}
}
Rust: types and a thin client
use serde::Deserialize;
#[derive(Deserialize)]
pub struct LatestResponse {
pub success: bool,
pub date: String,
pub base: String,
pub rates: std::collections::HashMap<String, f64>,
pub dates: std::collections::HashMap<String, String>,
pub currencies: std::collections::HashMap<String, String>,
}
pub struct EnergyApiClient {
api_key: String,
base_url: String,
client: reqwest::Client,
}
impl EnergyApiClient {
pub fn new(api_key: impl Into<String>) -> Self {
Self {
api_key: api_key.into(),
base_url: "https://energy-api.com/api/v1".to_string(),
client: reqwest::Client::new(),
}
}
pub async fn latest(&self, symbols: &[&str]) -> anyhow::Result<LatestResponse> {
if symbols.is_empty() {
anyhow::bail!("symbols required");
}
let mut url = reqwest::Url::parse(&format!("{}/latest", self.base_url))?;
url.query_pairs_mut()
.append_pair("symbols", &symbols.join(","))
.append_pair("api_key", &self.api_key);
let res = self.client.get(url).send().await?;
if !res.status().is_success() {
let text = res.text().await.unwrap_or_default();
anyhow::bail!(format!("HTTP {} {}", res.status(), text));
}
Ok(res.json::<LatestResponse>().await?)
}
}
Runtime validation and safety
- Guard symbol inputs with a whitelist fetched from /symbols at startup. Update periodically.
- Use discriminated unions or enums for categories so your feature flags (e.g., intraday vs daily) don’t rely on string comparisons.
- Propagate currencies alongside numeric values. Never drop units from your domain model.
Versioning the SDK
The API path includes /api/v1. Align your SDK’s major version with the upstream API version to make upgrade intent explicit. Suggested approach:
- SDK major version tracks API major (e.g., sdk v1.x.y for /api/v1).
- New optional fields in responses are non-breaking. Add them as optional types.
- Deprecations: mark methods or fields with comments and a change log entry, then bump minor when adding replacements and major when removing.
- Feature detection: when endpoints add fields like frequencies or currencies maps, parse them if present and default sensibly if not.
Offline Replay and Resilience
Energy apps run on laptops, trading floors, and servers behind strict firewalls. You’ll want your SDK to continue accepting writes (estimate requests) and scheduled reads even if the network blips. Pattern:
- Queue: When fetch fails with a 429 or transient network error, enqueue the request (method, path, params, timestamp) in durable storage (IndexedDB/LocalStorage on web; SQLite/disk on server/desktop).
- Backoff: Implement exponential backoff on 429 with jitter. Respect documented errors: 401/422 are permanent and should not be retried without code changes.
- Replay: A background worker drains the queue when connectivity resumes. Preserve original ordering for analytical consistency.
- De-dup: Since reads are idempotent, duplicates are safe; for POST /cost-estimate treat it as a read-style calculator (per docs), so duplicates are not side-effecting.
Example TypeScript snippet for a simple offline queue:
type QueuedReq = { path: string; params: Record<string, string>; enqueuedAt: number };
class RequestQueue {
constructor(private storageKey = "energy_api_queue", private client: EnergyApiClient) {}
private load(): QueuedReq[] {
try { return JSON.parse(localStorage.getItem(this.storageKey) || "[]"); } catch { return []; }
}
private save(q: QueuedReq[]) { localStorage.setItem(this.storageKey, JSON.stringify(q)); }
enqueue(path: string, params: Record<string, string>) {
const q = this.load();
q.push({ path, params, enqueuedAt: Date.now() });
this.save(q);
}
async replay() {
const q = this.load();
const remain: QueuedReq[] = [];
for (const item of q) {
try {
await (this.client as any)["get"]<unknown>(item.path, item.params);
} catch (e) {
remain.push(item); // keep if still failing
}
}
this.save(remain);
}
}
JavaScript example: read and use fields
This snippet calls /latest, reads rates and currencies, and prints normalized strings per symbol. It matches the cURL from Quick Start.
async function run() {
const url = new URL("https://energy-api.com/api/v1/latest");
url.searchParams.set("symbols", "BRENT_CRUDE,TTF_GAS,EUA_CO2");
url.searchParams.set("api_key", "YOUR_API_KEY");
const res = await fetch(url.toString());
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error || `HTTP ${res.status}`);
}
const data = await res.json();
// data.success, data.rates, data.currencies, data.dates
Object.keys(data.rates).forEach(sym => {
const val = data.rates[sym];
const cur = data.currencies[sym];
const d = data.dates[sym];
console.log(`${sym} ${val} ${cur} (as of ${d})`);
});
}
run().catch(console.error);
Practical integration details
- Units: Prices and intensities are returned in source-appropriate units (e.g., TTF_GAS in EUR/MWh, BRENT_CRUDE in USD/barrel, EUA_CO2 in EUR/MT, carbon intensity in gCO2eq/kWh). Use currencies and symbol metadata to label UI and avoid mixing units.
- Mixed bases: Many responses return base = "MIXED". Rely on currencies per symbol; if you need a unified currency, convert downstream in your app.
- Publishing schedules: Some markets don’t publish on weekends/holidays. /historical returns the most recent prior value when a date is non-publishing.
- Caching: You can safely cache daily series for symbols with frequency = "daily". For intraday electricity endpoints, expire cache per the date granularity (hourly/15-min). If you build a cache layer, key by path + serialized params + API key.
- Errors: Handle 401 (invalid/missing api_key), 404 (no data for symbols/date), 422 (validation errors), 429 (rate limit). Only retry on 429 or network failures with backoff.
Real-World Use Cases
- Price Alert System: Poll /latest with symbols=TTF_GAS,EUA_CO2,BRENT_CRUDE and trigger notifications when thresholds or percentage changes are crossed. Use /timeseries to compute rolling volatility and avoid alert storms.
- ESG Dashboard: Combine /emissions/latest and /carbon-intensity by country to show allowance prices alongside grid intensity. Add /timeseries for trend panels and tie country selection to symbol discovery via /symbols.
- Wholesale Cost Calculator: Call /electricity/pvpc or /electricity/latest for relevant retail reference or day-ahead symbols, then POST /cost-estimate with kwh_per_month to provide a quick customer estimate (note: this excludes taxes and network charges as documented).
FAQ
How often does the TTF gas price update?
TTF_GAS is exposed with frequency "daily" in the symbol metadata. Use /latest for the most recent print and /timeseries for historical days. Consider non-publishing days when scheduling polls.
Can I get historical energy prices going back multiple years?
Use /timeseries with your desired date range and symbols. The API returns a symbol-keyed date map with frequencies and currencies to help you build accurate charts.
Does the API support multiple commodities in one request?
Yes. Endpoints like /latest and /timeseries accept comma-separated symbols, allowing you to fetch gas, oil, carbon, coal, and electricity together in one call and one schema.
What should I do when I hit rate limits?
On 429 responses, implement exponential backoff with jitter and consider an offline queue for scheduled jobs. Do not retry 401 or 422 without fixing credentials or parameters.
How do I know the unit or currency for a symbol?
Use /symbols for metadata and the currencies map in responses like /latest and /timeseries. Never assume a global base; the API returns per-symbol currency codes.
Conclusion + CTA
Building a type-safe SDK for energy markets doesn’t have to be a multi-quarter project. With a single normalized schema, clear symbol metadata, and a few pragmatic patterns—strong typing, version-aligned releases, and offline replay—you can ship features that stay correct as you add commodities and endpoints.
Start by wrapping /symbols, /latest, /historical, and /timeseries in your client; enforce symbol and parameter types; and propagate currency and frequency alongside every value. From there, layering in dashboards, risk metrics, and cost models is straightforward.
Want to accelerate your roadmap? Explore the endpoints, copy the samples above, and iterate with production-grade data. Energy API gets you from zero to reliable energy data fast. Try Energy API for free and build your TypeScript and Rust integrations today.
Ready to get started?
Get your API key and start querying energy commodity prices in minutes.
Get API KeyRelated posts
Discover how to design an effective Developer SDK for the Energy API. Learn patterns for idiomatic clients, pa...
Read more →
Discover how to build offline-first mobile apps for field technicians using Energy API. Enhance decision-makin...
Read more →
Discover how to build a local sandbox for market microstructure testing using a finance API. Simulate order bo...
Read more →
If you build energy data products, you already know the hard part isn’t code—it’s contracts. Different provid...
Read more →
Discover how to effectively benchmark intraday trading algorithms using Finance API market feeds and synthetic...
Read more →