Only GET routes are versioned (.../v1 suffix); POST ingest and the plugin-facing prices GET are not
The plugins (Claude, Copilot, OpenCode) each live in their own separate repository, one per plugin, since they have fundamentally different data sources and cannot report the same data as each other. Within each plugin’s repository, every org’s install of that plugin is nonetheless the same build, while each org’s backend is deployed independently, on its own schedule. Frontend↔backend and plugin↔backend routes are versioned differently as a result:
-
Frontend ↔ backend GET routes. Frontend and backend of one deployment are versioned and deployed together by that org, so response shapes can change freely. These GET routes carry a
.../v1suffix (e.g./api/usage/claude/v1,/api/usage/all/series/v1) so a future breaking change can bump just that route to.../v2without touching the others. -
Plugin ↔ backend routes. The plugin build is shared and not forked per org, so it cannot hardcode a version: a bumped
/api/usage/ingest/clauderoute would instantly break every org still on the old backend. These routes (POST /api/usage/ingest/{claude,copilot,opencode}, andGET /api/prices/claude, which the Claude plugin also calls) stay unversioned and additive-only forever: only add optional fields, never remove or rename. This keeps the plugin usable straight from GitHub, with zero per-org adjustment.
Each versioned route carries its own .../v1 suffix at the end of its
path, not one version segment wrapping the whole API.
A breaking change to one route only bumps that route;
every other route stays untouched. /health stays unversioned
(infra probe, not part of the plugin contract).
Path suffix chosen over header/media-type versioning: visible and testable without extra tooling — an operator running their own fork can see the version in the URL/logs/curl output directly.