Versioning Internal Platform APIs

Last week, a platform team I know shipped what they called a “small cleanup” to their deployment API. They renamed a field from service_id to serviceId, removed an endpoint that was “barely used,” and updated the response format to match their new schema. No version bump. No deprecation notice. Just a Friday afternoon deploy and a Slack message in #platform-updates that nobody read.

Monday morning, 40 CI pipelines failed. Three teams scrambled to update their deployment scripts. A critical security hotfix got blocked because the team couldn’t deploy. The platform team spent the entire week doing emergency migrations instead of planned work. And the trust they’d built over the previous year? Gone.

This happens constantly with internal APIs. There’s a temptation to treat them differently than external ones: “we can just tell people to update,” “everyone’s in the same building,” “we’ll coordinate in Slack.” But internal APIs deserve more versioning discipline than external ones, not less.

Warning callout:

Internal customers are captive customers. They can’t switch to a competitor’s platform. This makes breaking their workflows worse, not better - they have no recourse except escalating to leadership or building workarounds that create tech debt.

The same practices that make external APIs predictable - semantic versioning, deprecation policies, migration support - apply to internal platform APIs. The difference is that internal teams have higher expectations because they’re colleagues, and lower patience because they have their own roadmaps that don’t include “emergency migration of the deployment API.”

Versioning Strategies

Choosing a Versioning Scheme

Four versioning approaches dominate API design, each with tradeoffs that matter for internal platforms.

URL path versioning
/api/v1/deployments and /api/v2/deployments is explicit and visible - every request clearly shows which version it's using. Load balancers can route traffic easily, and deprecating old versions is straightforward. For internal platform APIs, this is usually the right choice because visibility trumps elegance. When debugging a failed pipeline at 2 AM, you want to immediately know which API version the client is calling.
Header versioning and query parameter versioning
Accept: application/vnd.platform.v2+json and /api/services?version=2 both hide version information where developers forget to set it and debugging tools don't show it. Skip these unless you have specific requirements for URL stability.
Date-based versioning
/api/2024-01-15/services works well for APIs with frequent, incremental changes - Stripe and AWS use this successfully. The downside is version accumulation: after a few years, you have dozens of dated endpoints. This works best when you have the tooling to manage many concurrent versions.

For most internal platform APIs, I recommend URL path versioning. Maximum visibility, easy auditing, simple routing, and clear communication about which versions are approaching sunset.

Semantic Versioning for APIs

Semantic versioning gives version numbers meaning. For APIs, the interpretation is straightforward:

  • Major versions (v1 → v2)Indicate breaking changes that require consumer code updates. This includes removing endpoints, removing fields from responses, adding required fields to requests, changing field types, changing authentication mechanisms, or renaming fields without backward-compatible aliases. Major bumps need 90+ days advance notice and full deprecation process. Support the previous major version for at least 12 months.
  • Minor versions (v1.1 → v1.2)These are backward-compatible additions. New optional endpoints, new optional request fields, new response fields, new enum values - anything that existing client code can safely ignore. No advance notice required, just release notes. Consumers can upgrade without code changes.
  • Patch versions (v1.2.1 → v1.2.2)These are backward-compatible fixes. Bug fixes, performance improvements, documentation updates, security patches. These should be transparent to consumers - they shouldn't even notice. Apply patches to all supported versions.
Version bump decision guide.
Info callout:

The key question for any change: “Will existing client code break?” If yes, it’s a major version bump with full deprecation process. If no, it’s minor or patch. When in doubt, assume breaking - consumers will tell you if you’re being too conservative.

Breaking Change Management

Identifying Breaking Changes

The hardest part of API versioning isn’t the mechanics - it’s deciding whether a change is breaking in the first place. The core question is simple: will existing client code still work? If no, it’s breaking. If yes, you need to dig deeper: is the behavior meaningfully different? Will consumers notice or care?

Some changes are obviously breaking. Removing an endpoint returns 404 to anyone still calling it. Changing an HTTP method from POST to PUT breaks existing clients with 405 errors. Renaming a URL path parameter from {id} to {serviceId} makes existing URLs invalid. Adding a required field to requests means all existing requests fail validation. Changing a field’s type from string to number breaks deserialization. These require full deprecation process, no exceptions.

Some changes are obviously safe. Adding an optional request field doesn’t affect existing requests. Adding a field to responses is fine if clients ignore unknown fields (they should). New endpoints don’t touch existing ones. Loosening validation - accepting 200 characters where you previously accepted 100 - doesn’t break anything that worked before. Performance improvements and bug fixes are safe unless someone depends on the buggy behavior (it happens).

The tricky cases fall in between. Changing error codes from 400 to 422 is technically correct but breaks error handling logic. Changing pagination defaults affects clients that assumed a specific page size. Changing a field’s meaning - like making count include deleted items when it previously didn’t - is breaking even though the type and field name stay the same.

Changes that require full deprecation process.

Not every change demands that level of process. Many changes are additive or relaxing — they expand what the API accepts or returns without invalidating anything consumers already depend on. The distinction matters operationally because safe changes can ship continuously without coordination, while breaking changes gate on migration timelines and consumer readiness.

Misclassifying a safe change as a breaking change slows delivery unnecessarily; misclassifying a breaking change as safe causes outages. When the boundary is unclear — a changed default, a subtly different error shape — treat the change as breaking and let consumers confirm otherwise. It’s easier to relax a deprecation notice than to roll back a broken deployment.

Changes that can ship without deprecation.

When you’re unsure, treat it as breaking. Consumers will tell you if you’re being too conservative. Nobody complains about unnecessary deprecation warnings. They complain loudly about broken pipelines.

The Breaking Change Process

Once you’ve identified a breaking change, follow a structured process that gives consumers time to adapt.

Success callout:

Identify consumers before announcing changes. API gateway logs, service mesh telemetry, and SDK analytics tell you who’s actually using what. Targeted notification to affected teams gets better response than broadcast announcements that everyone ignores.

Deprecation Communication

Deprecation Timeline

A well-run deprecation follows a predictable timeline that gives consumers multiple opportunities to act - and escalating urgency as the deadline approaches.

  1. 1
    Major version sunsets
    Needs 12 months total. Start with a 30-day feedback period where you announce the proposal and gather input. Release the new version, then run both versions in parallel for 12 months. Send the 90-day warning, the 30-day warning, and finally sunset the old version. This sounds like a long time, but internal teams have their own roadmaps. A migration that takes 2 days of work might not get scheduled for 3 months.
  2. 2
    Minor feature deprecations
    These can move faster - 6 months is usually sufficient. Same pattern: announcement, parallel operation, escalating warnings, sunset.
  3. 3
    Security emergencies
    These are the exception. When you need to remove something immediately for security reasons, the minimum is 30 days with direct contact to every known consumer. Get security team approval, document the risk, and communicate urgently. Don't use this as a shortcut for normal deprecations - it burns trust.

Multi-Channel Communication

People miss messages. The developer who needs to migrate might be on vacation when you post the announcement. Their email might filter your notice to a folder they never check. Their team lead might forget to forward it. Successful deprecation communication uses multiple channels with increasing urgency.

The key insight is that urgency should escalate as the deadline approaches. Early communication is broad and informational - changelog updates, Slack posts, email announcements. As sunset nears, communication becomes targeted and direct: personal outreach to remaining consumers, calendar invites for the sunset date, escalation to team leads. The table below shows the progression.

Deprecation communication timeline.

Deprecation Headers and Response Warnings

Your API should actively warn consumers about deprecations, not just document them. HTTP headers are the standard mechanism:

HTTP/1.1 200 OK
Deprecation: true
Sunset: Sat, 15 Aug 2024 00:00:00 GMT
Link: </docs/migration/v1-to-v2>; rel="deprecation"
Standard HTTP deprecation headers.

These headers are machine-readable - consumers can build automation that alerts when their dependencies are deprecated. Combine headers with response body warnings for visibility in logs and debugging:

{
  "data": { },
  "_meta": {
    "deprecation": {
      "deprecated": true,
      "sunsetDate": "2024-08-15",
      "replacement": "/api/v2/deployments",
      "migrationGuide": "https://docs/migration/v1-to-v2"
    }
  }
}
Response body deprecation warning.

The _meta field convention keeps deprecation warnings separate from business data while ensuring they’re visible in API responses.

Warning callout:

Never surprise people with deprecations. Even if policy says “90 days notice,” earlier is better. Give consumers time to plan, prioritize, and execute migration - not scramble under deadline pressure.

Migration Support

Making Migration Easy

The faster consumers migrate, the sooner you can sunset the old version. Every friction point in migration extends the deprecation timeline. Invest in making migration as easy as possible - it pays back in reduced support load and faster adoption.

  • Tier 1:
    Self-Service
    Most migrations should be completable without platform team involvement.Provide a comprehensive migration guide with before/after code examples, a comparison table of old vs new API, an FAQ covering common questions, and a self-service validator that consumers can run to check their migration status. Support comes through async channels like #platform-help.
  • Tier 2:
    Assisted
    For consumers who need more help, offer scheduled office hours for questions, code review of migration PRs, and migration progress tracking.This tier catches teams who are stuck on edge cases or unclear about specific changes.
  • Tier 3:
    Hands-On
    For high-impact consumers or complex migrations, the platform team creates the migration PRs directly.This includes pair programming sessions and production deployment support. Reserve this for critical paths where a delayed migration blocks the entire deprecation.
Migration support tiers.

Migration Guide Structure

A good migration guide answers questions in the order developers ask them. Start with the overview: what’s changing, why, and the timeline. Then provide a quick start - the minimal changes for basic migration, ideally with copy-paste code snippets. A 5-minute migration path for simple cases gets momentum going.

Follow with detailed changes: endpoint-by-endpoint mapping, request/response format changes, authentication changes. Include before/after code examples for each language your consumers use. Cover edge cases explicitly - they’re where most migration bugs hide.

End with testing guidance: how to verify migration works, dual-write/dual-read patterns for gradual rollout, and rollback procedures if something goes wrong. Include a troubleshooting section addressing common errors.

A typical migration guide structure looks like this:

# Deployment API v1 → v2 Migration Guide

## Overview
- **What's changing**: Field naming conventions, response format
- **Timeline**: v1 sunset August 15, 2024
- **Estimated effort**: 30 minutes for most services

## Quick Start (5 minutes)
1. Update SDK: `npm install @platform/deploy-sdk@2`
2. Run the codemod: `npx @platform/deploy-codemod`
3. Verify in staging

## Detailed Changes
| v1 | v2 | Notes |
|----|----|---------|
| service_id | serviceId | Codemod handles this |
| GET /status | GET /deployments/{id}/status | New endpoint path |

## Testing Your Migration
- Run `platform-cli validate` to check for v1 usage
- Deploy to staging and verify deployment workflow

## Troubleshooting
- **"Unknown field service_id"**: Run the codemod again
- **404 on /status**: Update to new endpoint path
Example migration guide structure.

Automated Migration Tools

Manual migration doesn’t scale. When you have 50 consuming services, even a “simple” migration consumes hundreds of developer-hours. Automation tools reduce that dramatically.

Codemods automatically transform code from old API patterns to new ones. They parse source files, identify API calls matching v1 patterns, and rewrite them to v2. Good codemods flag changes that need manual review rather than silently transforming ambiguous cases.

For internal APIs, developing custom codemods is often worth the investment. You know exactly what patterns consumers use because you can grep the monorepo or scan consuming services. A codemod that handles 80% of cases automatically and flags the remaining 20% for manual review dramatically reduces migration friction. Facebook’s jscodeshift makes this tractable for JavaScript/TypeScript:

// jscodeshift transform for deployment API v1 → v2 migration
export default function transformer(file, api) {
  const j = api.jscodeshift;
  const root = j(file.source);

  // Transform service_id → serviceId in object properties
  root.find(j.Property, { key: { name: 'service_id' } })
    .forEach(path => {
      path.node.key.name = 'serviceId';
    });

  // Flag ambiguous cases for manual review
  root.find(j.CallExpression, { callee: { property: { name: 'deploy' } } })
    .forEach(path => {
      const args = path.node.arguments;
      if (args.length > 2) {
        // Complex call pattern - add review comment
        j(path).insertBefore(
          j.commentLine(' TODO: Review this deploy() call for v2 compatibility')
        );
      }
    });

  return root.toSource();
}
Custom jscodeshift codemod for internal API migration.

The key to good codemods is conservative transformation: change what you’re certain about, flag what you’re not. A codemod that silently breaks edge cases is worse than no codemod at all.

  • Compatibility adaptersProvides a translation layer that accepts v1 format and converts to v2 internally. This lets the platform team sunset the actual v1 implementation while giving consumers more time to migrate their code. The adapter logs usage for migration tracking, creating visibility into who still depends on the compatibility layer.
  • Migration validatorsScans codebases for v1 API usage and generate reports. Run them in CI to block PRs that introduce new v1 dependencies. Provide them as self-service tools so consumers can check their own migration progress.
  • Progress trackersAggregates data from API gateway logs, validator results, and team self-reports to show migration status across all consumers. The platform team uses this to identify holdouts and target support.
Success callout:

The easier migration is, the faster it happens. Codemods that automatically transform code, adapters that provide temporary compatibility, and validators that confirm migration completeness all reduce friction and accelerate adoption.

Measuring Success

Deprecation Metrics

You can’t manage what you can’t measure. Deprecation metrics tell you whether migration is on track, where consumers are stuck, and whether the deprecation is causing problems.

Timeline metrics keep the deprecation on schedule. Compare actual milestone dates to planned ones. The ultimate measure: did you successfully sunset the old version on the announced date?

The Migration Funnel

Think of migration as a funnel with stages: total consumers → aware → started migration → testing on v2 → production on v2 → v1 fully retired. Each stage has observable metrics.

  • Aware
    Measured by announcement reach - did your Slack post get emoji reactions? Did affected teams acknowledge the notification?
  • Started migration
    Shows up as PRs opened, branches created, or SDK upgrades in package files.
  • Testing on v2
    Means staging or dev traffic hitting the new version.
  • Production on v2
    The goal - production traffic on the new version, ideally with v1 traffic dropping to zero.

Here’s what a healthy migration funnel looks like in practice. For a deployment API v2 migration with 50 consuming services:

  • Week 1:50 total → 45 aware (90%) → 0 started. Normal - teams are reading the announcement.
  • Week 4:50 total → 50 aware (100%) → 30 started (60%) → 5 testing. Good velocity.
  • Week 8:50 total → 50 aware → 45 started (90%) → 35 testing (70%) → 20 production (40%). On track.
  • Week 12 (90 days out):50 total → 48 production (96%) → 2 holdouts identified. Target support at holdouts.

When consumers are stuck at a particular stage, investigate why. Often it’s a missing migration tool (stuck at “started”), unclear documentation (stuck at “testing”), or a blocking bug in v2 (stuck at “production”). Rarely is it resistance - most developers would rather move on than maintain compatibility with deprecated APIs.

Migration milestone targets.

What Happens at Sunset

When sunset day arrives, you need a clear cutover plan. The cleanest approach is returning HTTP 410 Gone with a response body pointing to the migration guide and v2 endpoint. This is unambiguous - the old version is dead, not broken.

Some teams prefer a softer approach: redirect v1 requests to v2 with a compatibility layer, logging each redirect for visibility. This buys stragglers more time but extends your maintenance burden. Only do this if you have genuine holdouts who can’t migrate in time and have escalated appropriately.

Whatever you choose, communicate it in advance. “On August 15, v1 will return 410 Gone” is clear. “On August 15, v1 will stop working somehow” creates anxiety. The sunset mechanism should be as predictable as the deprecation timeline.

Conclusion

Remember that developer who spent a week scrambling because someone shipped a “small cleanup” without versioning? The 40 broken pipelines, the blocked security hotfix, the trust that took months to rebuild? That’s what bad API versioning looks like. It’s very visible.

Good API versioning is invisible. Consumers barely notice migrations because they’re well-communicated, well-supported, and well-timed. The new version shows up with deprecation warnings months in advance. The migration guide makes the change trivial. By the time sunset arrives, everyone’s already moved on.

Getting there requires treating internal APIs with the same discipline as external ones. Internal customers deserve predictable, well-communicated changes - arguably more so, because they can’t switch providers when you break them. Use semantic versioning to signal intent clearly. Follow a structured deprecation process with real timelines and escalating communication. Invest in migration support that makes adoption easy: guides, codemods, adapters, validators.

Success callout:

Good API versioning is invisible - consumers barely notice migrations because they’re well-communicated, well-supported, and well-timed. Bad versioning is very visible: broken pipelines, emergency meetings, and angry Slack messages. Invest in the former to avoid the latter.

The payoff is substantial. Trust with internal teams that makes future changes easier. Faster adoption of new versions because consumers know what to expect. Reduced support burden because migrations are self-service. And the platform team’s time spent on planned work instead of emergency migrations.

Quick Reference

Key deprecation timeline thresholds.

Further Reading

Sharethisarticle

Enjoyed the read? Share it with your network.

Other things I've written