Status: accepted Date: 2026-02-12
The system requires a versioning strategy to support API evolution while maintaining stability for consumers:
- Stability requirement: Consumers need backward compatibility guarantees within an API version
- Breaking changes: Cryptographic APIs may require breaking changes (algorithm upgrades, security improvements, schema changes)
- Deployment constraint: Same server binary may need to serve multiple API versions simultaneously during migration periods
- Developer experience: API version should be immediately visible and discoverable in documentation and examples
- Migration support: Clear migration path for consumers when breaking changes are introduced
- Long-lived integrations: Financial and cryptographic integrations often run for years without updates
Adopt URL path-based versioning using /v1/* prefix for all API endpoints:
Route structure:
- All API endpoints under
/v1/*prefix (e.g.,/v1/secrets/*,/v1/transit/keys/:name/encrypt) - Health/metrics endpoints outside versioning:
/health,/ready,/metrics(not/v1/health) - Future breaking changes require new version path:
/v2/*
Contract definition:
- OpenAPI specification at
docs/openapi.yamldefines v1 contract baseline - Endpoint documentation in
docs/api/*.mddefines full public behavior - Breaking changes documented in
../../CHANGELOG.md
Version independence:
- No version in headers (no
Accept: application/vnd.secrets.v1+json) - No version in query parameters (no
?version=1or?api-version=v1) - Version specified solely in URL path
Coexistence strategy:
- Multiple versions can coexist:
/v1/*and/v2/*routes registered simultaneously - Gradual migration: consumers transition at their own pace
- Deprecation timeline: old versions maintained until safe to remove (based on consumer usage metrics)
Version specified in request headers: Accept: application/vnd.secrets.v1+json or X-API-Version: v1.
Rejected because:
- Less visible in browser/curl (requires inspecting request headers)
- Not discoverable from URL alone (must read documentation to know header format)
- Harder to cache at CDN/proxy level (varies by header)
- More complex routing configuration (must inspect headers, not just path)
- Developer experience suffers (copy-paste URL doesn't include version)
Version specified in query string: ?version=1 or ?api-version=v1.
Rejected because:
- Harder to route at API gateway/proxy level (requires query param inspection)
- Query parameters typically used for filtering/pagination, not versioning
- URL caching complexity (query params affect cache keys differently)
- Inconsistent with REST best practices (version is not a filter)
- Easy to forget in code (URLs work without version, fail unexpectedly)
Version in subdomain: v1.api.example.com vs v2.api.example.com.
Rejected because:
- DNS configuration complexity (must manage multiple DNS entries)
- TLS certificate management overhead (wildcard cert or multiple certs)
- Deployment complexity (routing traffic to correct version per subdomain)
- Higher operational burden for pre-1.0 system
- Acceptable for large-scale APIs, over-engineered for current needs
Single evolving API with deprecation warnings for old behavior.
Rejected because:
- Unacceptable for cryptographic API (breaking changes too risky)
- No clear contract boundary for consumers
- Cannot safely remove deprecated features (no version to sunset)
- Financial integrations require stability guarantees
- Migration path unclear (when is it safe to remove deprecated behavior?)
Benefits:
- URL clarity: API version immediately visible in every request
- Developer experience: Copy-paste examples work, no hidden header configuration
- Proxy/gateway friendly: Easy to route by path prefix (
/v1/*to v1 backend) - Documentation simplicity: All examples show version in URL path
- Caching friendly: URL fully determines version, standard HTTP caching applies
- Migration clarity:
/v2/*coexists with/v1/*, clear separation
Coexistence and migration:
- Gradual rollout: Deploy
/v2/*routes alongside/v1/* - Consumer autonomy: Clients migrate at their own pace (no forced upgrade)
- Monitoring: Track usage metrics per version to inform deprecation timeline
- Deprecation process:
- Announce deprecation timeline in release notes
- Monitor
/v1/*usage metrics to identify remaining consumers - Notify active consumers via support channels
- Remove deprecated version after safe sunset period (e.g., 6 months)
Breaking change process:
When breaking changes needed:
- Implement
/v2/*endpoints with new behavior - Document changes in
docs/releases/vX.Y.Z.md:- What changed (endpoint paths, request/response schemas, status codes)
- Migration examples (v1 request → v2 request)
- Deprecation timeline for v1
- Update
docs/openapi.yamlwith v2 contract - Announce in
../../CHANGELOG.mdand release notes - Monitor
/v1/*and/v2/*usage metrics - Remove
/v1/*after sunset period
Limitations:
- No per-field versioning: All fields in API version evolve together
- Cannot mix v1 and v2 fields in same request/response
- Acceptable: Simplifies implementation and consumer understanding
- URL length: Version prefix adds characters to URL
- Negligible impact:
/v1/adds only 4 characters
- Negligible impact:
- Routing complexity: Router must handle multiple version prefixes
- Acceptable: Gin route groups make this straightforward
Non-breaking changes:
These can be added to existing /v1/* without new version:
- Adding optional request fields
- Adding new response fields (consumers must ignore unknown fields)
- Adding new endpoints under
/v1/* - Clarifying documentation without behavior changes
Breaking changes (require /v2/*):
- Changing endpoint paths or required path parameters
- Removing response fields or changing field meaning/type
- Changing required request fields or accepted formats
- Changing status code semantics for successful behavior
Future considerations:
- Could add version in response header for debugging:
X-API-Version: v1 - Could implement automatic v1-to-v2 adapter middleware for common cases
- Could add version negotiation for advanced use cases (not needed now)