SaaS API versioning

SaaS API Versioning Without Breaking Existing Clients

SaaS API versioning protects existing integrations by keeping their request formats, responses, and behavior stable while you introduce changes. The safest approach combines explicit version selection with parallel support, contract tests, and a published retirement process.

A renamed field can break a Zapier workflow, while a changed default can alter billing without producing an error. Start by defining which changes need a new contract, then roll them out without moving existing clients automatically.

Decide which changes need a new API version

Version your external contract when a change would require an existing client to behave differently. Internal refactoring, database upgrades, and performance improvements don’t need new API versions unless they alter observable behavior.

Backward-compatible changes preserve existing behavior

Adding an endpoint or an optional request parameter usually preserves compatibility. Adding response fields also works when clients tolerate unfamiliar properties.

Stripe’s compatibility policy classifies new resources, optional request parameters, and response properties as backward-compatible changes. That policy also tells integrations how to handle additions.

However, a new enum value can break an exhaustive switch statement or a no-code condition. Treat it as compatible only when your documented contract permits unknown values and supported consumers handle them.

Breaking changes include meaning, not just structure

Removing a field, renaming it, changing its type, or making an optional parameter mandatory breaks the contract. So can changing pagination defaults, sorting, error codes, or authentication requirements.

Changing money values from cents to dollars is breaking even if the field remains numeric. Preserve the old meaning under the old version.

Security fixes need a documented exception process. Version pinning must never guarantee continued access to a vulnerability.

Choose one version-selection contract

URI, header, and media-type versioning all support explicit selection. Choose based on your clients and infrastructure, then document one authoritative selection mechanism.

Microsoft’s API design guidance describes these approaches and their trade-offs.

ApproachHow clients select itOperational consideration
URI versioningA path such as /v1/customersURLs expose the version, and routing must maintain each supported path.
Header versioningA dedicated request headerGateways, logs, and caches must account for that header.
Media-type versioningA versioned representation in AcceptClients and servers must handle content negotiation consistently.

The selection method matters less than predictable behavior when a version is missing or unsupported.

Pin the requested behavior

Stripe provides a concrete header-based example. A customer-list request can use GET /v1/customers?limit=1 with Stripe-Version: 2024-09-30.acacia, plus authorization.

For an account with no customers, the response body has this shape: {"object":"list","data":[],"has_more":false,"url":"/v1/customers"}.

The /v1/ path doesn’t select Stripe’s dated contract. Stripe also uses SDK-pinned versions or an account default when no request override applies.

Define missing and unsupported versions

For an existing unversioned API, preserve its legacy behavior when callers omit a selector. Don’t reinterpret omission as “latest.”

Reject unsupported versions with a documented error. Never silently substitute another contract.

Step 1: Write the compatibility policy before implementation

Your policy should fit in a short document that developers and integration owners can understand. Store it alongside your API specification.

Record the complete contract

Maintain an OpenAPI document for each supported version. Include request requirements, response schemas, status codes, pagination, authentication scopes, and rate-limit behavior.

Also document semantics that schemas can’t fully capture. Explain monetary units, timestamp formats, retry behavior, and whether omitted fields differ from explicit null values.

Record the current production baseline before changing anything. Include webhook formats, SDK defaults, and important automation mappings.

Assign ownership and support commitments

Give the release a named owner, an approval process, and a review period. A solopreneur can hold every role, but the responsibilities still need to be explicit.

Define the minimum support window and emergency exceptions. GitHub, for example, promises at least 24 months of support for a previous REST API version after a newer version ships.

Don’t promise a window your team can’t maintain. Every supported contract needs tests, documentation, security maintenance, and an operational owner.

Step 2: Run old and new behavior side by side

Build version handling around shared business logic where practical. Keep version-specific validation and serialization at clearly defined boundaries.

Route requests to explicit adapters

Resolve the version at the gateway or application entry point. Pass the resolved value through validation, business operations, and response serialization.

An adapter translates between a version’s external format and your internal model. Stripe describes this general approach in its API versioning architecture.

However, don’t recreate old behavior through guesswork. If a transformation loses information, retain the necessary data or keep separate behavior.

For header-based representations, configure caches to distinguish versions. An appropriate Vary response header helps prevent one client receiving another version’s cached representation.

Expand storage before removing old structures

Add new database fields first, backfill them, and keep the old version functional throughout migration. Delay destructive schema changes until the old contract no longer needs them.

Keep deployment scope narrow. A version rollout and an unrelated billing redesign shouldn’t share one release.

Reverting application code won’t restore deleted columns or repair records already changed by a new version. Rollback needs a data-recovery plan as well.

Step 3: Test contracts and business outcomes

A successful HTTP response doesn’t prove compatibility. Tests must establish that each supported client still gets the expected outcome.

Automate schema and consumer checks

Use OpenAPI comparisons, such as oasdiff, to flag structural changes in pull requests. Pair these checks with runtime tests because schema comparisons can’t detect changed business meaning.

Pact can capture consumer expectations when you control both sides of an integration. For public APIs, maintain fixtures for supported SDKs and critical workflows.

Include contract updates, test changes, and owner approval in the same pull request. For new validation rules, start in warning mode to measure false positives before rejecting customer requests.

Exercise failure paths and stored results

Use synthetic records and staging credentials. Live payment connections and customer-facing email services don’t belong in compatibility tests.

Cover authorization failures, cross-tenant access, pagination boundaries, malformed input, and timeouts after a database commit. Assert stored state as well as response bodies.

For write operations, confirm retries don’t duplicate charges, subscriptions, or records. Compare business totals against the baseline, including currency and billing-period boundaries.

Finally, test existing no-code mappings. A valid JSON response can still break an automation that expects a particular field path.

Step 4: Pilot with monitoring and a rehearsed rollback

Deploy the new implementation before asking customers to migrate. Keep existing clients on their current contract and make adoption deliberate.

Expand through stable client cohorts

Start with internal integrations, then opt-in customers. Assign cohorts by tenant or integration identity so one workflow doesn’t alternate between versions.

Track traffic by version, tenant, endpoint, and release. Compare latency, validation failures, authorization errors, retries, and webhook failures.

Also monitor business outcomes. Duplicate invoices or missing CRM records matter even when error rates remain flat. Agree on acceptable differences before launch, and avoid logging secrets or unnecessary personal data.

Advance only after the pilot meets its acceptance criteria through a representative usage period.

Define rollback triggers before launch

Record who can pause migration, how to disable the new adapter, and which measurements trigger that action. Include corrupted writes and missing business events alongside technical failures.

Rehearse the procedure in staging. Measure how long it takes to stop traffic, restore behavior, and confirm recovery.

A rollback must preserve the caller’s contract. Don’t redirect a client that expects the new schema to an incompatible old response.

Retain changed record IDs and prior values where appropriate. Reverting code alone won’t repair affected data.

Step 5: Publish the migration and retire deliberately

Give customers an actionable migration guide before announcing a shutdown date. Include changed fields, behavior differences, request examples, SDK implications, and webhook instructions.

Set milestones and send usable notices

A starter schedule might announce availability on day 0, offer migration support by day 30, and review adoption on day 60. Retirement on day 90 should happen only if your support commitments permit it.

Use email and dashboards as well as machine-readable notices. The Deprecation response header communicates deprecation; the Sunset header specification addresses expected unavailability.

State the affected version, replacement, deadline, and consequence of missing it. Avoid ambiguous messages such as “upgrade soon.”

Migrate webhooks and confirm remaining consumers

Treat webhook payloads as separate contracts. Stripe documents a staged migration using two webhook endpoints with different configured versions, including endpoints distinguished by a query parameter.

Test the new handler before switching processing. If both versions deliver events, deduplicate them so the same business operation runs once. Also test out-of-order delivery and retries.

Before retirement, inventory SDKs, scheduled jobs, connectors, and automations still using the old contract. Quiet traffic doesn’t prove abandonment because some integrations run monthly.

Retire only after the published commitments are met. Return the documented retirement response rather than silently upgrading remaining callers.

Keep Client Behavior Predictable

Reliable SaaS API versioning depends on contract stability, explicit selection, and evidence that both versions work. A version identifier alone can’t protect integrations from changed semantics or unsafe defaults.

Start with a compatibility policy and a production baseline. Then use parallel behavior, targeted tests, and measured adoption to make migration reversible.

Retirement should complete a migration customers understand, with no surprise changes to the behavior they depend on.

About the author

The SAAS Podium

View all posts

Leave a Reply

Your email address will not be published. Required fields are marked *