Drıftstack docs
Docs

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.json is 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 %XX form: field-不明 is reported as field-%E4%B8%8D%E6%98%8E. A comma inside a name becomes %2C, so it cannot be mistaken for the separator. Decode with decodeURIComponent if 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:

  1. Announce the deprecation in a Deprecation HTTP response header on every affected endpoint, with a Sunset header pointing at the declared removal date (RFC 8594).
  2. Document the migration path in the OpenAPI spec via deprecated: true on the affected operation / field, plus a description pointing at the replacement.
  3. 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.
  4. Minimum 90 days between announcement and removal. Longer for high-impact changes (e.g. session lifecycle shape).
  5. 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 a Deprecation / Sunset window.
  • 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 via purpose + archetype fields; shape changes within the lifecycle (e.g. new states, new required fields) are breaking and trigger the deprecation cycle.
  • /v1/webhooks/* — WebhookEventType enum 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 at docs.driftstack.io/webhooks/events). Customers are encouraged to subscribe with explicit events: [...] 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 + Sunset response headers in production logs. Generic SDK middleware can surface these automatically.
  • Log x-driftstack-unknown-fields in 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.