API versioning strategy
Versioning policy for the HTTP API surface (/v1/* and any later
major prefix). Distinct from the SDK versioning policy at
docs.driftstack.io/sdk/versioning: SDKs version
independently of the API; this doc covers the API endpoint contract.
TL;DR
- One major version active at a time.
/v1/*today. - Additive changes are free — new endpoints, new optional request fields, new response fields, new enum values. Customers don’t break.
- Breaking changes go through a deprecation cycle, then a new
major version.
/v2/*only when justified; not on a calendar. - The one exception is a security fix: a breaking change that closes a credential or authorization leak ships immediately, and is announced when it ships.
- The OpenAPI spec at
/openapi.jsonis the contract. It is generated from the same definitions the server enforces.
What counts as additive vs breaking
| Change | Class |
|---|---|
| New endpoint | Additive |
| New optional request field with sensible default | Additive |
| New response field | Additive |
| New enum value (sent BY server, e.g. webhook event types) | Breaking for closed-enum consumers |
| New enum value (accepted FROM client, e.g. tier IDs) | Additive (server is permissive) |
| Renaming an existing field | Breaking |
| Removing an existing field | Breaking |
| Changing a field’s type (e.g. number → string) | Breaking |
| Tightening a validation constraint | Breaking |
| Loosening a validation constraint | Additive |
| Changing default behaviour of an existing endpoint | Breaking |
| Changing HTTP status code returned | Breaking |
| Changing error type URI in problem-detail | Breaking |
| Adding a new error type URI | Additive |
| Changing rate-limit caps | Operational; not contract |
The “new enum value” row deserves emphasis: when the server
sends a closed enum value the client doesn’t know about (e.g.
a new webhook_event_type), strictly-typed clients break. SDKs
mitigate this with a passthrough escape hatch, but the contract
itself is breaking — bump major version, OR ship the new value
behind a feature flag, OR add a transitional period where both
old and new values are emitted.
Unrecognised request fields
Request bodies are parsed permissively: a top-level field the
endpoint does not declare is ignored, and the request succeeds
exactly as if it had not been sent. That keeps adding a field
additive rather than breaking, but on its own it means a typo is
presented to you as success — a mistyped archetype on profile
creation would return 200 OK with the default substituted.
So the ignored keys are named back to you. When an authenticated write carries fields the endpoint doesn’t declare, the response includes:
x-driftstack-unknown-fields: descriptoin
For example, PATCH /v1/profiles/{id} with body
{"name": "Updated", "descriptoin": "Checkout flow"} returns
200 OK, applies the name change, and sets the header to
descriptoin — the description update was silently dropped.
Specifics worth knowing:
- Comma-separated key names, in the order the body listed them.
- Top-level keys only. Nested unknown keys are not reported; polymorphic and forward-compat payloads make nested reporting too noisy to be useful.
- At most 10 keys, each truncated to 64 characters, so a large body cannot inflate the response.
- Percent-encoded, so that any field name can be reported without
changing the response. An ASCII name — every field this API
declares — appears unchanged. A name containing a character an HTTP
header cannot carry, such as a non-Latin script or an emoji, appears
in
%XXform:field-不明is reported asfield-%E4%B8%8D%E6%98%8E. A comma inside a name becomes%2C, so it cannot be mistaken for the separator. Decode withdecodeURIComponentif you display the value. - The header is absent when every field was recognised — treat presence, not value, as the signal.
- Reporting, never rejecting. The status code and response body are identical with and without unknown fields, so nothing breaks by starting to send this header, and nothing breaks if you ignore it.
- Not emitted on unauthenticated auth endpoints — signup, login, password reset, magic link, email verification, token refresh and logout. On those, check your field names against the request body in the API reference, which lists every field each one accepts.
Because it is purely additive, this header is not part of the breaking-change contract above: treat it as a diagnostic, and do not parse it as a stable machine interface.
Deprecation cycle for breaking changes
When a breaking change is necessary, the sequence is:
- Announce the deprecation in a
DeprecationHTTP response header on every affected endpoint, with aSunsetheader pointing at the declared removal date (RFC 8594). - Document the migration path in the OpenAPI spec via
deprecated: trueon the affected operation / field, plus adescriptionpointing at the replacement. - Email the accounts that use it. An account that called the deprecated surface in the last 30 days gets one notice by email, not a recurring reminder.
- Minimum 90 days between announcement and removal. Longer for high-impact changes (e.g. session lifecycle shape).
- Remove the surface in the next major version OR — if the change is small enough to fit within the existing major — ship it as a separate operation while leaving the old one in place for a defined sunset window.
Security fixes
A breaking change that closes a credential or authorization leak — a request that could read, redirect or use a secret, or reach something the caller is not allowed to, through a surface that should not allow it — does not wait for the deprecation cycle above. Every day of notice would be a day the leak stays open, so:
- It ships immediately, on
/v1/*, without aDeprecation/Sunsetwindow. - It is announced under Security in the API changelog when it ships: what was accepted before and what is refused now, the exact error a refused request gets, and what to send instead.
- Where the affected accounts can be identified, those customers are contacted. Not every account a fix affects can be identified in advance, so the changelog entry is the announcement every customer can rely on.
- It is as narrow as the fix allows. Only the requests that could carry the leak change, and they are refused with an error that says what to send — never silently accepted with a different meaning.
The SDK CHANGELOGs carry the same note, under Security, for the method it affects.
When a new major version is justified
/v2/* ships when:
- A breaking change can’t be avoided (e.g. a session lifecycle redesign with different states and transitions).
- Multiple breaking changes batch sensibly (don’t spread breakage across many minor announcements when one batched cut is cleaner).
- An entirely new architectural shape requires a distinct contract (for example, REST semantics changing to RPC).
It does NOT ship when:
- Pre-1.0-style restlessness wants to “clean things up.” We
instead deprecate + phase out within
/v1/*. - A single field rename is desired. Announce, deprecate, support both for a sunset window, drop the old name.
Operating multiple majors simultaneously
When more than one major is active:
/v1/*continues to work for the announced sunset window (typically 12+ months).- Both versions share the same auth + rate limits.
- The OpenAPI spec documents both.
- Customers can pin a version via the URL prefix; no header-based versioning today.
Per-resource versioning notes
/v1/sessions/*— session-lifecycle shape changes require a new major version. Customers already opt into schema evolution viapurpose+archetypefields; shape changes within the lifecycle (e.g. new states, new required fields) are breaking and trigger the deprecation cycle./v1/webhooks/*—WebhookEventTypeenum is closed. Adding a new event type IS technically breaking for strictly-typed consumers. We mitigate via the SDK passthrough pattern + documented “we may send unknown event types; ignore + continue” (the catalog of all event types lives atdocs.driftstack.io/webhooks/events). Customers are encouraged to subscribe with explicitevents: [...]arrays so the server only ever sends event types the customer already opted into. New event types are then additive at the wire level; subscription is opt-in./v1/account/*— the account reads in the API reference (rate limits, AI spend, saved proxies). Same external-facing breaking-change discipline as/v1/sessions/*.- Routes that are not in the API reference — the dashboard’s own account, billing and sign-in routes, and Driftstack’s internal routes — are not covered by this policy.
What you should do
- Pin to a specific major in your integration. SDKs handle this by encoding the major in the URL constants they ship.
- Subscribe explicitly to the webhook events you handle; ignore + continue on unknown event types (defensive parsing).
- Watch the
Deprecation+Sunsetresponse headers in production logs. Generic SDK middleware can surface these automatically. - Log
x-driftstack-unknown-fieldsin non-production. Its presence means a field you sent was ignored — usually a typo, occasionally a field removed on our side. - Read the CHANGELOG for the SDK you use; SDK CHANGELOGs cross-reference API-side deprecations relevant to that language’s surface.
What we don’t do
- Header-based versioning (
API-Version: 2024-05-01) — considered but rejected. URL-prefix is more discoverable, easier to debug in logs, and matches industry convention (Stripe-style/v1/). - Date-based versioning per-account (Stripe’s “API version pinning”) — not offered. Driftstack uses explicit URL prefixes.
- Continuous breaking changes — pre-1.0 SDKs ship them
under their published SemVer policy. Customers pinned to the
HTTP
/v1/*contract receive additive-only changes, apart from security fixes.
Related
- Webhook event catalog:
docs.driftstack.io/webhooks/events. - Rate-limit policy:
docs.driftstack.io/reference/rate-limits. - Error handling:
docs.driftstack.io/sdk/error-handling. - OpenAPI spec is served at
/openapi.jsonon the live API host (api.driftstack.dev/openapi.json) and rendered as a browsable reference by Scalar UI atapi.driftstack.dev/docs.