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 Path | Status | Release Date | Deprecation Date | Corresponding EnerOS Version |
|---|---|---|---|---|
/api/v1/... | Stable | 2026-01 | — | v0.40+ |
/api/v2/... | Preview | 2026-09 (planned) | — | v0.50+ |
/api/v3/... | Planning | 2027 (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 Type | Example | Compatibility | URL Version | Description |
|---|---|---|---|---|
| PATCH | Bug fixes, performance optimization, documentation updates | Fully compatible | Unchanged | 0.47.0 → 0.47.1 |
| MINOR | New endpoints, new fields, new enum values | Backward compatible | Unchanged | 0.47.0 → 0.48.0 |
| MAJOR | Endpoint deletion, field semantic change, behavior change | Breaking | +1 | 0.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
| Header | Value | Description |
|---|---|---|
Accept | application/graphql; version=YYYY-MM | Specify API month version |
Accept | application/graphql+json; version=2026-07 | Explicitly specify JSON response |
Schema-Version | 2026-07 | Schema version (equivalent to Accept) |
GraphQL version naming uses the YYYY-MM format, with a new version released each quarter:
| Version | Status | Release Date | Deprecation Date |
|---|---|---|---|
2026-01 | Deprecated | 2026-01 | 2026-07 |
2026-04 | Deprecated | 2026-04 | 2026-10 |
2026-07 | Current stable | 2026-07 | 2027-01 |
2026-10 | Planned | 2026-10 | 2027-04 |
4. WebSocket Subprotocol Versioning
WebSocket negotiates versions via subprotocol (Sec-WebSocket-Protocol):
Sec-WebSocket-Protocol: eneros.v1
| Subprotocol | Status | Release Date | Deprecation Date |
|---|---|---|---|
eneros.v1 | Stable | 2026-01 | — |
eneros.v2 | Planning | 2027 | — |
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
| Level | Description | Guarantee Period |
|---|---|---|
| Stable | Interface is finalized, only accepts backward-compatible changes | 12 months after next major version release |
| Preview | Interface may change, not recommended for production use | No guarantee, may change at any time |
| Experimental | Experimental feature, may be removed | No guarantee |
| Deprecated | About to be offline, should migrate as soon as possible | 6-9 months after announcement |
Compatibility Rules
The following changes are considered backward compatible and do not change the version number:
| Change | Compatibility | Example |
|---|---|---|
| Add endpoint | ✅ | Add GET /api/v1/topology/{id} |
| Add optional request field | ✅ | Add optional description field |
| Add response field | ✅ | Add updated_at to response |
| Add enum value | ✅ | Add archived to status |
| Change field from optional to required | ❌ | Breaking, requires new version |
| Field semantic change | ❌ | v changes from per-unit to nominal value |
| Delete endpoint | ❌ | Delete GET /api/v1/old_endpoint |
| Delete field | ❌ | Delete 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 Header | Description |
|---|---|
Deprecation | true indicates the response contains deprecated fields |
Sunset | Expected offline date of the endpoint/field |
Link: rel="successor-version" | New version endpoint location |
Warning | Human-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
| Phase | Duration | Support | Documentation Label |
|---|---|---|---|
| Planning | 1-3 months | None | RFC |
| Preview | 3-6 months | best-effort | Preview |
| Stable | 12-24 months | Full | None |
| Maintenance | 6-12 months | Security fixes only | Maintenance |
| Deprecated | 6 months | Security fixes only | Deprecated |
| Offline | — | None | 410 Gone |
Current Version Status
| Version | Status | Released | Entered Maintenance | Deprecation Announced | Offline |
|---|---|---|---|---|---|
| v1 | Stable | 2026-01 | — | — | — |
| v2 | Preview | 2026-09 | — | — | — |
| GraphQL 2026-01 | Deprecated | 2026-01 | 2026-07 | 2026-07 | 2027-01 |
| GraphQL 2026-04 | Deprecated | 2026-04 | 2026-10 | 2026-10 | 2027-04 |
| GraphQL 2026-07 | Stable | 2026-07 | — | — | — |
Deprecation Process
Deprecation Phases
Each endpoint or field deprecation follows a standardized process:
| Phase | Duration | Action | Client Perception |
|---|---|---|---|
| 1. Announcement | 6 months | Document and response header annotate deprecated | Response header Deprecation: true |
| 2. Warning | 3 months | Calling deprecated endpoint returns warning logs | Response header Warning: 299 |
| 3. Rate Limiting | 1 month | Deprecated endpoint rate limit tightened to 10% | Frequent 429 responses |
| 4. Offline | — | Remove endpoint, return 410 Gone | Call 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 Field | v2 Field | Description |
|---|---|---|
bus_count | topology.buses.count | Nested |
branch_count | topology.branches.count | Nested |
created_at | metadata.created_at | Metadata grouping |
v | voltage.magnitude | Semantic |
theta | voltage.angle | Semantic |
Endpoint Path Changes
| v1 Endpoint | v2 Endpoint | Description |
|---|---|---|
POST /networks/{id}/powerflow | POST /networks/{id}:runPowerFlow | Adopt Google API naming convention |
GET /agents/{id}/status | GET /agents/{id}:getStatus | Action endpoints use colon |
POST /timeseries/batch | POST /timeseries:batchWrite | Action endpoints use colon |
Behavior Changes
| Behavior | v1 | v2 | Description |
|---|---|---|---|
| Power flow default method | newton_raphson | newton_raphson | Unchanged |
| Power flow default timeout | 30000ms | 60000ms | Increased timeout |
| Time series aggregation timezone | UTC | UTC (supports tz parameter) | Enhanced |
| Pagination default size | 20 | 50 | Increased default value |
| Error code naming | INVALID_REQUEST | INVALID_REQUEST | Unchanged |
Migration Steps
- Audit current API usage: Statistics of in-use endpoints via audit logs
- Read migration guide: Evaluate change scope against field mapping table
- Use version-aware SDK: SDK automatically handles version differences
- Gradual switch: First switch to v2 in test environment, validate then progressively roll out to production
- Monitor deprecation warnings: Observe
Warning: 299response headers to identify missed deprecated endpoints - 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 Type | Description | Tool |
|---|---|---|
| Contract testing | Verify response structure conforms to Schema | schemathesis |
| Snapshot testing | Compare response JSON snapshots | insta |
| Integration testing | End-to-end call verification | testcontainers |
| Regression testing | Old version endpoints not broken | cargo 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
Related Documentation
- API Overview — API type overview
- REST API — Current v1 endpoints
- GraphQL — GraphQL version negotiation
- WebSocket — Subprotocol versions
- eneros-api crate — API service implementation
- CHANGELOG — Complete changelog
- ADR Index — Architecture Decision Records