Skip to main content

Version Management

API Reference

API Version Management

EnerOS adopts a dual strategy of semantic versioning + URL path versioning, ensuring that clients do not need frequent upgrades during API evolution while allowing breaking changes to be introduced. Version management covers all four API types: REST, GraphQL, WebSocket, and SSE. Each protocol’s version negotiation method differs slightly, but they share the same set of compatibility guarantees and deprecation processes.

Versioning Strategy

1. URL Path Versioning (REST / SSE)

Each major version is distinguished by URL path, with the version number in the path aligned with the EnerOS major version:

Version PathStatusRelease DateDeprecation DateCorresponding EnerOS Version
/api/v1/...Stable2026-01v0.40+
/api/v2/...Preview2026-09 (planned)v0.50+
/api/v3/...Planning2027 (planned)v1.0+
http://localhost:8080/api/v1/networks      # Current stable version
http://localhost:8080/api/v2/networks      # Next major version (in development)

Rules:

  • Major version change = breaking change (endpoint deletion, field semantic change, behavior change)
  • Minor and patch versions do not change the URL path
  • Within the same major version, new endpoints/fields are introduced additively, backward compatible
  • Multiple major versions can coexist, with parallel support during the transition period

2. Semantic Versioning (SemVer)

The overall EnerOS version follows the MAJOR.MINOR.PATCH rules:

Change TypeExampleCompatibilityURL VersionDescription
PATCHBug fixes, performance optimization, documentation updatesFully compatibleUnchanged0.47.0 → 0.47.1
MINORNew endpoints, new fields, new enum valuesBackward compatibleUnchanged0.47.0 → 0.48.0
MAJOREndpoint deletion, field semantic change, behavior changeBreaking+10.47.x → 1.0.0

3. Header Version Negotiation (GraphQL)

GraphQL does not use URL versioning, negotiating API version and feature set via Headers:

POST /graphql HTTP/1.1
Accept: application/graphql; version=2026-07
Content-Type: application/json
HeaderValueDescription
Acceptapplication/graphql; version=YYYY-MMSpecify API month version
Acceptapplication/graphql+json; version=2026-07Explicitly specify JSON response
Schema-Version2026-07Schema version (equivalent to Accept)

GraphQL version naming uses the YYYY-MM format, with a new version released each quarter:

VersionStatusRelease DateDeprecation Date
2026-01Deprecated2026-012026-07
2026-04Deprecated2026-042026-10
2026-07Current stable2026-072027-01
2026-10Planned2026-102027-04

4. WebSocket Subprotocol Versioning

WebSocket negotiates versions via subprotocol (Sec-WebSocket-Protocol):

Sec-WebSocket-Protocol: eneros.v1
SubprotocolStatusRelease DateDeprecation Date
eneros.v1Stable2026-01
eneros.v2Planning2027

Clients can declare multiple subprotocols during handshake, and the server selects the highest supported version:

const ws = new WebSocket('ws://localhost:8080/ws', ['eneros.v1', 'eneros.v2']);
console.log(ws.protocol); // Server-selected version, e.g., "eneros.v1"

Compatibility Guarantees

Compatibility Levels

LevelDescriptionGuarantee Period
StableInterface is finalized, only accepts backward-compatible changes12 months after next major version release
PreviewInterface may change, not recommended for production useNo guarantee, may change at any time
ExperimentalExperimental feature, may be removedNo guarantee
DeprecatedAbout to be offline, should migrate as soon as possible6-9 months after announcement

Compatibility Rules

The following changes are considered backward compatible and do not change the version number:

ChangeCompatibilityExample
Add endpointAdd GET /api/v1/topology/{id}
Add optional request fieldAdd optional description field
Add response fieldAdd updated_at to response
Add enum valueAdd archived to status
Change field from optional to requiredBreaking, requires new version
Field semantic changev changes from per-unit to nominal value
Delete endpointDelete GET /api/v1/old_endpoint
Delete fieldDelete legacy_field
Change default value⚠️Depending on impact scope, may require new version

Field Deprecation Annotation

Deprecated fields are annotated simultaneously via response headers and response body:

HTTP/1.1 200 OK
Deprecation: true
Sunset: Sun, 06 Jul 2027 00:00:00 GMT
Link: </api/v2/networks>; rel="successor-version"
Warning: 299 - "The 'bus_count' field is deprecated, use 'buses.count' instead"
Response HeaderDescription
Deprecationtrue indicates the response contains deprecated fields
SunsetExpected offline date of the endpoint/field
Link: rel="successor-version"New version endpoint location
WarningHuman-readable warning with 299 status code

GraphQL field deprecation is annotated via @deprecated:

type Network {
  busCount: Int! @deprecated(reason: "Use 'buses.count' instead")
  buses: [Bus!]!
}

API Version Lifecycle

Each major version goes through a complete lifecycle:

Planning → Preview → Stable → Maintenance → Deprecated → Offline
  │      │       │       │       │       │
  │      │       │       │       │       └─ Returns 410 Gone
  │      │       │       │       └─ Security fixes only, documented
  │      │       │       └─ No new features, bug fixes only
  │      │       └─ Primary version, full support
  │       └─ Available but may change, documented as "Preview"
  └─ RFC / ADR discussion phase
PhaseDurationSupportDocumentation Label
Planning1-3 monthsNoneRFC
Preview3-6 monthsbest-effortPreview
Stable12-24 monthsFullNone
Maintenance6-12 monthsSecurity fixes onlyMaintenance
Deprecated6 monthsSecurity fixes onlyDeprecated
OfflineNone410 Gone

Current Version Status

VersionStatusReleasedEntered MaintenanceDeprecation AnnouncedOffline
v1Stable2026-01
v2Preview2026-09
GraphQL 2026-01Deprecated2026-012026-072026-072027-01
GraphQL 2026-04Deprecated2026-042026-102026-102027-04
GraphQL 2026-07Stable2026-07

Deprecation Process

Deprecation Phases

Each endpoint or field deprecation follows a standardized process:

PhaseDurationActionClient Perception
1. Announcement6 monthsDocument and response header annotate deprecatedResponse header Deprecation: true
2. Warning3 monthsCalling deprecated endpoint returns warning logsResponse header Warning: 299
3. Rate Limiting1 monthDeprecated endpoint rate limit tightened to 10%Frequent 429 responses
4. OfflineRemove endpoint, return 410 GoneCall failure

Deprecation Response Example

Phase 1: Announcement

HTTP/1.1 200 OK
Deprecation: true
Sunset: Sun, 06 Jul 2027 00:00:00 GMT
Link: </api/v2/networks/net_8f3a2b>; rel="successor-version"
Warning: 299 - "GET /api/v1/networks/{id}/legacy_powerflow is deprecated, use POST /api/v1/networks/{id}/powerflow instead"
Content-Type: application/json

{"data": {"id": "net_8f3a2b", "bus_count": 14}}

Phase 4: Offline

{
  "error": {
    "code": "GONE",
    "message": "Endpoint GET /api/v1/networks/{id}/legacy_powerflow has been removed. Use POST /api/v1/networks/{id}/powerflow instead.",
    "details": {
      "removed_at": "2027-07-06T00:00:00Z",
      "successor": "/api/v1/networks/{id}/powerflow",
      "doc_url": "https://docs.openeneros.com/api-reference/versioning#migration-guide"
    }
  },
  "request_id": "req_20260706_001"
}

GraphQL Deprecation

GraphQL field deprecation via @deprecated directive, clients can retrieve during Introspection:

query {
  __type(name: "Network") {
    fields {
      name
      isDeprecated
      deprecationReason
    }
  }
}

Response:

{
  "data": {
    "__type": {
      "fields": [
        {"name": "busCount", "isDeprecated": true, "deprecationReason": "Use 'buses.count' instead"},
        {"name": "buses", "isDeprecated": false, "deprecationReason": null}
      ]
    }
  }
}

Migration Guide

v1 → v2 Migration Example

Field Renaming

v1 response:

curl http://localhost:8080/api/v1/networks/net_8f3a2b
{
  "id": "net_8f3a2b",
  "name": "IEEE-14",
  "bus_count": 14,
  "branch_count": 20,
  "created_at": "2026-07-06T08:30:00Z"
}

v2 response (semantic fields, using nested structure):

curl http://localhost:8080/api/v2/networks/net_8f3a2b
{
  "id": "net_8f3a2b",
  "name": "IEEE-14",
  "topology": {
    "buses": {"count": 14},
    "branches": {"count": 20}
  },
  "metadata": {
    "created_at": "2026-07-06T08:30:00Z"
  }
}

Migration mapping table:

v1 Fieldv2 FieldDescription
bus_counttopology.buses.countNested
branch_counttopology.branches.countNested
created_atmetadata.created_atMetadata grouping
vvoltage.magnitudeSemantic
thetavoltage.angleSemantic

Endpoint Path Changes

v1 Endpointv2 EndpointDescription
POST /networks/{id}/powerflowPOST /networks/{id}:runPowerFlowAdopt Google API naming convention
GET /agents/{id}/statusGET /agents/{id}:getStatusAction endpoints use colon
POST /timeseries/batchPOST /timeseries:batchWriteAction endpoints use colon

Behavior Changes

Behaviorv1v2Description
Power flow default methodnewton_raphsonnewton_raphsonUnchanged
Power flow default timeout30000ms60000msIncreased timeout
Time series aggregation timezoneUTCUTC (supports tz parameter)Enhanced
Pagination default size2050Increased default value
Error code namingINVALID_REQUESTINVALID_REQUESTUnchanged

Migration Steps

  1. Audit current API usage: Statistics of in-use endpoints via audit logs
  2. Read migration guide: Evaluate change scope against field mapping table
  3. Use version-aware SDK: SDK automatically handles version differences
  4. Gradual switch: First switch to v2 in test environment, validate then progressively roll out to production
  5. Monitor deprecation warnings: Observe Warning: 299 response headers to identify missed deprecated endpoints
  6. Remove v1 calls: Complete all migrations before v1 goes offline

SDK Version Negotiation

eneros-sdk automatically handles version negotiation and field mapping:

use eneros_sdk::ApiClient;

let client = ApiClient::builder()
    .endpoint("http://localhost:8080")
    .api_version("v2")           // Specify version
    .token("<bearer-token>")
    .auto_migrate(true)           // Enable auto-migration (v1 → v2 field mapping)
    .build()?;

let network = client.networks().get("net_8f3a2b").await?;
// v2 SDK directly returns nested structure
println!("{:?}", network.topology.buses.count);
from eneros_sdk import ApiClient

client = ApiClient(
    endpoint="http://localhost:8080",
    api_version="v2",
    token="<bearer-token>",
    auto_migrate=True,
)
network = client.networks.get("net_8f3a2b")
print(network.topology.buses.count)
import { ApiClient } from '@eneros/sdk';

const client = new ApiClient({
  endpoint: 'http://localhost:8080',
  apiVersion: 'v2',
  token: '<bearer-token>',
  autoMigrate: true,
});
const network = await client.networks.get('net_8f3a2b');
console.log(network.topology.buses.count);

Changelog

Each MINOR / MAJOR release synchronously updates CHANGELOG.md and API reference documentation. Major changes release an ADR (Architecture Decision Record) explaining the motivation and migration path.

Changelog Format

# CHANGELOG

## [0.48.0] - 2026-07-06

### Added
- Added `GET /api/v1/topology/{id}` endpoint for querying grid topology
- Added `POST /api/v1/networks/{id}:validate` endpoint
- Added `tags` field to `NetworkInput`
- Added `paused` value to `AgentStatus` enum

### Changed
- Default timeout for `POST /networks/{id}/powerflow` adjusted from 30s to 60s
- Default `page_size` for pagination adjusted from 20 to 50

### Deprecated
- `Network.bus_count` field deprecated, use `Network.buses.count` instead
- `GET /agents/{id}/status` endpoint deprecated, use `GET /agents/{id}` instead

### Fixed
- Fixed power flow calculation panic in islanding scenarios
- Fixed event loss after WebSocket reconnection

### Security
- Upgraded `rustls` to 0.23.x, fixed CVE-2026-1234

ADR (Architecture Decision Record)

Major changes release an ADR, recording the decision background, solution comparison, and final choice:

docs/adr/
├── 0001-url-path-versioning.md
├── 0002-graphql-header-versioning.md
├── 0003-websocket-subprotocol-versioning.md
├── 0004-v2-field-nesting.md
└── 0005-deprecation-process.md

ADR template:

# ADR-0004: v2 Field Nesting

- Status: Accepted
- Date: 2026-06-15
- Decision Makers: API Working Group

## Background
v1 response fields are flattened, making it difficult to understand field ownership as the number of fields grows...

## Solution Comparison
- Option A: Keep flat + prefix (`network_bus_count`)
- Option B: Nested grouping (`topology.buses.count`) ✅
- Option C: GraphQL-ize (all via GraphQL)

## Decision
Adopt Option B, reason...

## Impact
- Clients need to update field access paths
- SDK provides auto_migrate automatic mapping

Compatibility Testing

EnerOS maintains a compatibility test suite to ensure version evolution does not break existing contracts:

Test TypeDescriptionTool
Contract testingVerify response structure conforms to Schemaschemathesis
Snapshot testingCompare response JSON snapshotsinsta
Integration testingEnd-to-end call verificationtestcontainers
Regression testingOld version endpoints not brokencargo test

Compatibility tests run automatically in CI, and any breaking changes are detected and block merging.

Multi-Version Coexistence

EnerOS supports multiple major versions running in parallel, with v1 and v2 both available during the transition period:

http://localhost:8080/api/v1/networks   # v1 endpoint
http://localhost:8080/api/v2/networks   # v2 endpoint

The data layer is shared, with only the interface layer differing. The same resource has consistent IDs in v1 and v2, and can be referenced across versions.

Multi-Version Configuration

[api]
enabled_versions = ["v1", "v2"]
default_version = "v1"
v1_sunset_at = "2027-07-06"  # v1 planned offline date
v2_preview = true              # Whether v2 is a preview version

Version Negotiation Best Practices

Client Fixed Version

Production environments recommend fixing the API version to avoid surprises from automatic upgrades:

let client = ApiClient::builder()
    .endpoint("http://localhost:8080")
    .api_version("v1")        // Fixed v1
    .pin_minor_version("0.47") // Fixed minor version
    .build()?;

Monitor Deprecation Warnings

Clients should monitor Warning and Deprecation response headers to proactively identify endpoints that need migration:

import logging

response = client.get("/networks/net_8f3a2b")
if response.headers.get("Deprecation") == "true":
    sunset = response.headers.get("Sunset", "unknown")
    warning = response.headers.get("Warning", "")
    logging.warning(f"Called deprecated endpoint, offline time: {sunset}, details: {warning}")

Progressive Migration

1. Use v1 exclusively
2. Use v2 for new features (mixed phase)
3. Gradually migrate v1 calls to v2
4. After v1 goes offline, use v2 exclusively