| name | api-deprecation |
| description | Plan and execute API deprecation without breaking consumers. Outputs deprecation timeline, sunset headers, migration guides, consumer tracking, and communication templates. |
| argument-hint | ["API version","consumer count","migration complexity","timeline","replacement API"] |
| allowed-tools | Read, Write |
API Deprecation
Deprecating an API without breaking clients requires advance notice, clear migration paths, monitoring of usage, and multiple communication channels. Rushed or silent deprecations destroy developer trust and cause production incidents. A well-run deprecation is a product launch for the new API.
Deprecation Process
- Measure usage. Who is calling the deprecated endpoint? What volume? Can you identify consumers from API keys or User-Agent headers?
- Define the timeline. Minimum notice period: 3 months for minor changes, 6 months for major. Published sunset date.
- Build the replacement first. Deprecate only when the replacement is production-ready.
- Add sunset headers. HTTP Deprecation and Sunset headers on every deprecated response.
- Communicate proactively. Email registered developers, changelog, docs, status page.
- Track migration progress. Monitor deprecated endpoint traffic declining toward zero.
- Sunset gradually. Move to 429 responses before full removal.
- Remove after zero traffic. Only retire the code when traffic is zero for 30+ days.
Deprecation Headers
from fastapi import FastAPI, Request, Response
from datetime import datetime
import pytz
DEPRECATED_ENDPOINTS = {
"/api/v1/orders": {
"deprecated_at": "Wed, 01 Jan 2025 00:00:00 GMT",
"sunset_at": "Sun, 01 Jun 2025 23:59:59 GMT",
"link": "https://docs.example.com/api/v2/orders",
"replacement": "/api/v2/orders",
},
"/api/v1/users/{user_id}/profile": {
"deprecated_at": "Fri, 01 Mar 2024 00:00:00 GMT",
"sunset_at": "Mon, 01 Sep 2024 23:59:59 GMT",
"link": "https://docs.example.com/api/v2/users",
"replacement": "/api/v2/users/{user_id}",
},
}
@app.middleware("http")
async def add_deprecation_headers(request: Request, call_next):
response = await call_next(request)
for pattern, config in DEPRECATED_ENDPOINTS.items():
if matches_path(request.url.path, pattern):
response.headers["Deprecation"] = config["deprecated_at"]
response.headers["Sunset"] = config["sunset_at"]
response.headers["Link"] = (
)
track_deprecated_usage(
endpoint=request.url.path,
method=request.method,
api_key=request.headers.get(),
user_agent=request.headers.get(),
)
response
Sunset Enforcement
from datetime import datetime, timezone
@app.middleware("http")
async def enforce_sunset(request: Request, call_next):
for pattern, config in DEPRECATED_ENDPOINTS.items():
if matches_path(request.url.path, pattern):
sunset = datetime.strptime(config["sunset_at"],
"%a, %d %b %Y %H:%M:%S GMT")
sunset = sunset.replace(tzinfo=timezone.utc)
now = datetime.now(timezone.utc)
days_until_sunset = (sunset - now).days
if 0 < days_until_sunset <= 30:
response = await call_next(request)
response.headers["Warning"] = (
f'299 - "This API endpoint will stop working in {days_until_sunset} days. '
f'Migrate to {config["replacement"]} now."'
)
return response
if now > sunset:
from fastapi.responses import JSONResponse
return JSONResponse(
status_code=410,
headers={
"Sunset": config["sunset_at"],
"Link": f'<{config["link"]}>; rel="successor-version"',
},
content={
: ,
: ,
: config[],
: config[],
}
)
call_next(request)
Consumer Tracking
import pandas as pd
from datetime import datetime, timedelta
class DeprecationTracker:
def __init__(self, db):
self.db = db
async def get_migration_progress(self, endpoint: str, days: int = 30) -> dict:
"""Track which consumers are still using deprecated endpoint."""
since = datetime.utcnow() - timedelta(days=days)
usage = await self.db.execute("""
SELECT
api_key,
consumer_name,
COUNT(*) as call_count,
MAX(called_at) as last_seen,
MIN(called_at) as first_seen
FROM deprecated_endpoint_usage
WHERE endpoint = $1 AND called_at >= $2
GROUP BY api_key, consumer_name
ORDER BY call_count DESC
""", [endpoint, since])
prev_period = await self.db.execute("""
SELECT api_key, COUNT(*) as call_count
FROM deprecated_endpoint_usage
WHERE endpoint = $1
AND called_at BETWEEN $2 AND $3
GROUP BY api_key
""", [endpoint, since - timedelta(days=days), since])
prev_counts = {row["api_key"]: row["call_count"] for row in prev_period}
consumers = []
for row in usage:
prev = prev_counts.get(row["api_key"], 0)
change_pct = ((row[] - prev) / prev * ) prev >
consumers.append({
: row[][:] + ,
: row[],
: row[],
: row[].isoformat(),
: change_pct < - (change_pct) < ,
})
{
: endpoint,
: (consumers),
: (c[] c consumers),
: consumers,
: ([c c consumers c[] == ]) / ((consumers), ),
}
Migration Guide Template
# Migration Guide: /api/v1/orders → /api/v2/orders
**Deprecated:** January 1, 2025
**Sunset date:** June 1, 2025
**Migration time estimate:** 2-4 hours
## What's Changing
The v2 Orders API improves on v1 in several ways:
- Pagination uses `cursor` instead of `offset` (more efficient for large datasets)
- `status` field now accepts an array for multi-status filtering
- Response includes `meta.total` for count without a separate request
## Breaking Changes
| v1 | v2 | Notes |
|----|----|----|
| `?page=2&limit=20` | `?cursor=<token>&limit=20` | Cursor-based pagination |
| `?status=paid` | `?status[]=paid&status[]=shipped` | Array filter |
| Response: `total_count` | Response: `meta.total` | Renamed |
| Response: `order_ref` | Response: `order_id` | Renamed (same value) |
## Migration Steps
### Step 1: Update the endpoint URL
```diff
- GET /api/v1/orders
+ GET /api/v2/orders
Step 2: Update pagination
response = client.get("/api/v1/orders", params={"page": 2, "limit": 20})
cursor = None
while True:
params = {"limit": 20}
if cursor:
params["cursor"] = cursor
response = client.get("/api/v2/orders", params=params)
data = response.json()
process(data["items"])
cursor = data["meta"].get("next_cursor")
if not cursor:
break
Step 3: Update status filter
client.get("/api/v1/orders", params={"status": "paid"})
client.get("/api/v2/orders", params={"status[]": ["paid", "shipped"]})
Testing Your Migration
We provide a v2 sandbox environment:
Need Help?
## Communication Templates
```markdown
## Email: Initial Deprecation Notice
Subject: Action Required: /api/v1/orders will be retired on June 1, 2025
We're writing to let you know that our API v1 orders endpoint will be retired on June 1, 2025.
**Your account:** We see you're calling /api/v1/orders approximately 500 times/day.
**What you need to do:** Migrate to /api/v2/orders before June 1, 2025.
**Migration guide:** https://docs.example.com/api/v2/migration
**Estimated migration time:** 2-4 hours
Key changes in v2:
- Cursor-based pagination (more efficient)
- Array filtering for status
- Minor field renames (see migration guide)
Questions? Reply to this email or join our migration office hours (Tuesdays 2-3pm UTC).
---
## Email: 30-Day Warning
Subject: URGENT: /api/v1/orders retires in 30 days — action required
Your account is still making ~N calls/day to /api/v1/orders, which retires on June 1, 2025.
After June 1, these calls will return HTTP 410 Gone.
Please migrate immediately: https://docs.example.com/api/v2/migration
If you need help or more time, contact us now.
Anti-Patterns to Avoid
| Anti-Pattern | Problem | Fix |
|---|
| Deprecating without a replacement | Consumers can't migrate | Build and test replacement before announcing deprecation |
| Too-short notice period | Developers can't get approvals and ship changes in time | 3-6 months minimum based on complexity |
| Silent deprecation | Consumers don't know until it breaks | Email + docs + headers + changelog |
| Removing before zero traffic | Breaking live consumers | Monitor traffic; retire only after 30 days of zero traffic |
| No consumer tracking | Can't know if migration is complete | Track per-API-key usage of deprecated endpoints |
| Returning 404 on sunset | Confusing error; no migration info | Return 410 Gone with migration guide URL |
| One communication only | Single email missed or ignored | 3 touchpoints: deprecation day, 30-day warning, 7-day warning |
10 Rules
- Build and production-test the replacement before announcing the deprecation.
- Sunset date is a firm commitment — don't extend it repeatedly; it destroys credibility.
- Add HTTP
Deprecation and Sunset headers on the first day of deprecation.
- Track every API key calling the deprecated endpoint — you need to know who hasn't migrated.
- Proactive outreach: email consumers when you deprecate, at 30 days, and at 7 days.
- Return 410 Gone (not 404) after sunset — with the migration guide URL in the response body.
- Sandbox the v2 API from day 1 of deprecation — consumers need a safe place to test migration.
- Document every breaking change in the migration guide — no surprises.
- Never remove code until traffic has been zero for 30+ days.
- Post-mortem any consumer broken by a deprecation — improve the process, not just the apology.