Olympus Docs
CookbookTokens & OAuth2

API versioning with scopes

How to evolve an OAuth2-protected API without breaking clients

You have v1 of your API live, with api:read and api:write scopes. You want to launch v2. How?

Versioning strategies

Path-based versioning

/api/v1/orders
/api/v2/orders

Two parallel implementations. Scopes are the same:

api:read   → can read on v1 or v2
api:write  → can write on v1 or v2

Client opts into version via URL. Scopes don't need to change.

Scope-per-version

api.v1:read    api.v1:write
api.v2:read    api.v2:write

v2 introduces new scope names. Old clients keep v1 scopes; new clients get v2.

Pros: explicit, easy to deprecate v1 (don't issue api.v1 scopes to new clients). Cons: scope sprawl.

Header-based versioning

GET /api/orders
Accept: application/vnd.your-app.v2+json

URL stays the same. Server dispatches by Accept header. Scopes don't change.

Recommendation for Olympus

Path-based + new scopes for additive new functionality.

api:read    → r/o on existing data
api:write   → r/w on existing data
api:admin   → admin endpoints (new in v2)
api:billing → billing endpoints (new in v2)

Don't version the scope itself unless the meaning of api:read changed (rare).

Deprecation

When announcing v1 deprecation:

  1. Communicate timeline (e.g., 12 months).
  2. Sunset header on v1 responses:
    Sunset: Sat, 31 Dec 2026 23:59:59 GMT
    Deprecation: true
    Link: <https://your-domain.com/api/v2>; rel="successor-version"
  3. Email clients using v1 monthly with usage stats.
  4. Hard-cut after sunset; v1 returns 410 Gone.

Token lifecycle during deprecation

Tokens issued before deprecation continue working for their lifetime (30d for refresh). Don't break them mid-flight.

New tokens issued after deprecation: optional warnings via custom headers or OAuth2 client metadata.

Per-client API versions

Pin clients to specific API versions:

hydra update client <id> \
  --metadata '{"api_version": "v2"}'

Your API reads the metadata after introspection:

const introspect = await introspectToken(token);
const version = introspect.client_metadata?.api_version ?? "v1";
const handler = handlers[version];
return handler(req);

Useful for B2B where each customer might be on different versions.

Scope migrations

You realize api:read covers too much. Split into:

  • orders:read
  • users:read
  • inventory:read

Migration:

  1. Issue tokens with both old and new for transition period:
    api:read orders:read users:read inventory:read
  2. Server accepts both (api:read OR orders:read).
  3. After 6 months, server only accepts new.
  4. Old api:read still valid technically; just not useful.

Don't break tokens. Always overlap.

Documenting scopes

In your API reference:

## GET /api/v2/orders

Requires scope: `orders:read`

Returns the user's orders.

Be explicit per endpoint. Don't make consumers guess.

OpenAPI spec:

paths:
  /orders:
    get:
      security:
        - oauth2: [orders:read]

Scope marketing

Some scopes sound scary ("api:admin", "users:delete"). Use friendly names in consent UI:

const labels = {
  "orders:read": "View your orders",
  "orders:write": "Modify your orders",
  "billing:write": "Manage your billing",
};

Display these in Hera's consent screen.

Public API vs Internal

Internal scopes (used only by your own apps): less curation needed. Public API (3rd party developers): high care, scope names are forever.

On this page