API Deprecation Notice Template
A template for communicating API deprecations, breaking changes, and sunset timelines to consumers.
Overview
APIs evolve. Fields are renamed, endpoints are replaced, and old versions are retired. Without a clear deprecation notice, consumers discover breaking changes only after their integrations fail. This template gives you a standard format for announcing deprecations, communicating timelines, and guiding consumers through migrations. Related resources: Stream LLM Output with Server-Sent Events (SSE), How to Document an API with OpenAPI, Swagger UI and Redoc, and Implement API Logging and Audit Trails.
When to Use
- For alternatives, see API Changelog Template.
Use this resource when:
- Removing or renaming an API endpoint, field, or parameter
- Shutting down an entire API version
- Migrating consumers from a legacy service to a replacement
- Updating authentication mechanisms that break existing clients
Solution
# API Deprecation Notice: `<Endpoint / Field / Version>`
**API:** `api.example.com/v1/...`
**Deprecated Since:** `2026-07-01`
**Sunset Date:** `2026-10-01` (92 days notice)
**Severity:** `Breaking Change` | `Non-Breaking Deprecation`
## What is Changing
### Before
GET /v1/orders?customer_id=123 Response: { “order_id”: “abc”, “total”: 100.00 }
### After
GET /v2/orders?customerId=123 Response: { “orderId”: “abc”, “totalAmount”: 100.00 }
## Why This Change is Happening
- Align field naming with company-wide camelCase standard
- Consolidate v1 and v2 data models to reduce maintenance overhead
- Remove deprecated fields that expose internal identifiers
## Migration Steps
1. **Update field names:** Rename `customer_id` to `customerId`, `order_id` to `orderId`
2. **Update response parsing:** Replace `total` with `totalAmount` (same data type)
3. **Switch endpoint:** Change base path from `/v1/orders` to `/v2/orders`
4. **Test in sandbox:** Validate integration against `sandbox-api.example.com/v2`
5. **Deploy to production:** Before `2026-10-01`
## Timeline
| Milestone | Date | Action Required |
|-----------|------|-----------------|
| Notice Sent | 2026-07-01 | Review migration guide |
| Sandbox Available | 2026-07-01 | Begin testing v2 endpoints |
| v1 Marked Deprecated | 2026-07-01 | Monitor deprecation headers |
| Final Reminder | 2026-09-01 | Complete migration or request extension |
| v1 Sunset | 2026-10-01 | v1 returns 410 Gone |
## Support & Contact
- **Migration Guide:** https://docs.example.com/api-migration
- **Sandbox Environment:** https://sandbox-api.example.com
- **Support Email:** api-support@example.com
- **Office Hours:** Every Tuesday 10:00 UTC
## Exceptions
If you cannot migrate before the sunset date, contact us at api-support@example.com with:
- Your use case
- Estimated migration timeline
- Blockers preventing timely migration
Explanation
The template separates what is changing from why and how to migrate. The timeline table creates accountability and removes ambiguity about deadlines. Including a sandbox environment and support contact reduces friction for consumers. The exceptions section acknowledges that not all consumers can migrate on the same schedule.
How to Adapt This Template
The same skeleton works for every deprecation, but three decisions change what you send:
Severity. A breaking change (removed endpoint, renamed field, auth scheme change) needs the full notice: migration steps, timeline table, sandbox link, and a named contact. A non-breaking deprecation (new preferred field alongside the old one, an optional parameter going away) can drop to a shorter notice — what changed, when it goes away, and where to read more. Don’t cry wolf: if you mark every deprecation “Breaking Change”, consumers stop reading the banner.
Audience. External consumers need dates they can plan around (quarters, not weeks) and a human they can escalate to. Internal consumers tolerate 30-day windows and a Slack post, but they still need the migration steps; “we’ll figure it out” is how internal v1s live forever. If your API has both audiences, write the public notice first and strip it down for the internal channel rather than the other way around.
Channel. Email reaches the people who registered; response headers reach the integrations that actually call you; dashboards reach the humans who debug at 2am. You need all three, which is why the communication plan below staggers them instead of sending one announcement.
For calibration, look at how public APIs announce theirs: Stripe’s versioning docs treat every change as dated and opt-in, GitHub publishes deprecation posts months ahead with exact UTC cutoff times, and Google’s API services publish explicit deprecation schedules per version. The common thread is that the deadline is a promise — pick a date you can actually hold.
Real-World Deprecation Notices
Three public deprecations worth studying before you write yours:
GitHub: password authentication for Git operations. GitHub announced in 2019 that password auth to the API would die, gave more than a year of notice, and — the part worth stealing — ran scheduled brownouts: for a few hours at a time, they rejected password-authenticated requests so integrations failed loudly before the real cutoff. Anyone still using a password found out during the brownout, not after the sunset. The final cutoff moved once, with the new date announced the same way.
Kubernetes: the deprecation policy itself is the notice. Kubernetes publishes a versioned deprecation policy — beta APIs get roughly 9 months or 3 releases of support after deprecation, GA versions longer. Consumers don’t need an email to plan; the policy IS the timeline. That’s the ideal end-state for an API with a large consumer base: the deprecation rules are public, versioned, and boringly predictable.
Stripe: date-pinned versions instead of a sunset. Stripe avoids the problem entirely for small changes — every account is pinned to the API version that was current when it was created, and consumers upgrade on their own schedule. They still sunset old versions eventually, but the version pinning means the deprecation notice lands on a codebase that’s already testing the new version, not one that just broke.
The pattern across all three: the deadline is real, the warning arrives through the channel the consumer actually uses, and there’s a rehearsal (brownout, pinned version, published policy) before the hard cutoff.
Deprecation Header Implementation
The Deprecation and Sunset HTTP headers (draft IETF standard) let client code detect deprecated endpoints programmatically. Implement them in your API middleware.
Express.js Middleware
function deprecationMiddleware(req, res, next) {
const deprecatedPaths = {
"/v1/orders": { sunset: "2026-10-01", replacement: "/v2/orders" },
"/v1/products": { sunset: "2026-10-01", replacement: "/v2/products" },
};
const match = Object.keys(deprecatedPaths).find((path) =>
req.path.startsWith(path)
);
if (match) {
const info = deprecatedPaths[match];
res.setHeader("Deprecation", "true");
res.setHeader("Sunset", new Date(info.sunset).toUTCString());
res.setHeader(
"Link",
`<https://docs.example.com/api-migration>; rel="deprecation"`
);
}
next();
}
app.use(deprecationMiddleware);
Python Flask Middleware
from datetime import datetime
from flask import Flask, request, g
DEPRECATED_PATHS = {
"/v1/orders": {"sunset": "2026-10-01", "replacement": "/v2/orders"},
"/v1/products": {"sunset": "2026-10-01", "replacement": "/v2/products"},
}
@app.before_request
def add_deprecation_headers():
for path, info in DEPRECATED_PATHS.items():
if request.path.startswith(path):
g.deprecation_sunset = info["sunset"]
g.deprecation_replacement = info["replacement"]
break
@app.after_request
def set_deprecation_headers(response):
if hasattr(g, "deprecation_sunset"):
response.headers["Deprecation"] = "true"
response.headers["Sunset"] = datetime.strptime(
g.deprecation_sunset, "%Y-%m-%d"
).strftime("%a, %d %b %Y 00:00:00 GMT")
response.headers["Link"] = (
'<https://docs.example.com/api-migration>; rel="deprecation"'
)
return response
Tracking Migration Progress
Watch traffic on the deprecated endpoints — it’s the only reliable way to know who hasn’t migrated, since nobody reads email.
SQL Query for Deprecation Traffic
SELECT
endpoint,
COUNT(*) AS request_count,
COUNT(DISTINCT client_id) AS unique_clients,
MAX(timestamp) AS last_request
FROM api_requests
WHERE endpoint LIKE '/v1/%'
AND timestamp >= NOW() - INTERVAL '7 days'
GROUP BY endpoint
ORDER BY request_count DESC;
Alerting on Stale Consumers
Set up an alert when a consumer with significant traffic hasn’t started migrating:
alert: stale_deprecation_consumer
expr: |
sum by (client_id) (
rate(api_requests_total{endpoint=~"/v1/.*"}[1h])
) > 10
for: 24h
labels:
severity: warning
annotations:
summary: "Client {{ $labels.client_id }} still using deprecated v1 endpoints"
description: "This client has made >10 req/h to v1 endpoints in the last 24h"
Communication Plan
| Channel | Timing | Audience | Content |
|---|---|---|---|
| Email blast | T-90 days | All registered consumers | Full deprecation notice + migration guide link |
| Blog post | T-90 days | Public | Announcement + context for the change |
| API response headers | T-90 days | Active integrations | Deprecation: true, Sunset header |
| Dashboard banner | T-60 days | Dashboard users | Persistent banner with migration deadline |
| Follow-up email | T-30 days | Non-migrated consumers | Reminder + offer office hours |
| Direct outreach | T-14 days | High-traffic non-migrated | Personal email or call from API team |
| Final email | T-7 days | All remaining | ”v1 sunset in 7 days” |
| Status page | T-0 | All | v1 returns 410 Gone, status page update |
The point of the stagger isn’t nagging: it’s that each channel catches a different group. The email goes to whoever registered the key. The headers go to the code that calls you. The banner catches the human who logs in to debug. Direct outreach is for the handful of consumers who generate enough traffic to matter but haven’t responded to anything else.
Variants
| Context | Approach | Notes |
|---|---|---|
| Public API | 90+ days notice, blog post, email | Consumer trust depends on predictable timelines |
| Internal API | 30 days notice, Slack announcement | Faster iteration, smaller consumer base |
| Emergency security patch | 7 days notice, direct outreach | Security takes priority over convenience |
| GraphQL API | @deprecated directive + notice | Schema-level deprecation alongside communication |
What Works
- Send deprecation headers in API responses at least 90 days before sunset (
Deprecation: true,Sunset: <date>) - Provide a working replacement before removing the old endpoint
- Maintain a changelog with all deprecations and migrations
- Track migration progress by monitoring traffic to deprecated endpoints
- Offer office hours or a migration guide for complex changes
- Use multiple communication channels: email alone isn’t enough
- Log deprecation header usage so you know which clients are aware
Common Mistakes
- Announcing deprecation without a replacement: consumers have nowhere to go
- Too-short notice periods: enterprise clients need quarters to plan changes
- Changing behavior silently without announcing deprecation first
- Not tracking which consumers still use deprecated endpoints
- Hard-deleting without a grace period: always return 410 Gone first
- Sending one email and assuming everyone read it: use multiple channels
- Not providing a sandbox for consumers to test the new endpoint
- Extending the sunset date repeatedly: undermines trust in future deadlines
- Forgetting to update SDKs and client libraries alongside the API change
Troubleshooting
- Consumers claim they never got the notice: they didn’t read the email. This is expected, and it’s why the headers, banner, and reminders exist. Check your request logs: if a client still hits v1 at T-30, they get a reminder; at T-14 with real traffic, they get a person.
- A client’s integration breaks before the sunset date: usually a field rename that shipped early or a client that parses responses too strictly. Diff the v1 response against what the notice documented : if the notice was accurate, the fix is on their side and your migration guide covers it; if it wasn’t, you owe an updated notice.
- A major consumer can’t migrate in time: grant a written extension with an expiration date, log it, and make them the first line of your final notice. Never extend the public sunset for everyone to accommodate one consumer; that trains the rest to ignore your deadlines.
- Traffic to v1 hasn’t dropped by T-30: your deprecation headers probably aren’t reaching clients. Verify with a curl against a deprecated endpoint — if
Deprecation: trueisn’t in the response, the middleware isn’t deployed where the traffic actually terminates (load balancer, gateway, or a legacy route that bypasses it). - Someone asks for an exception after sunset: the answer is already in the exceptions section: there’s no “one more week” after 410 Gone starts returning, because the alternative is teaching every future notice that your deadlines are negotiable.
See Also
- The Deprecation HTTP Header — IETF draft: the specification behind the
DeprecationandSunsetheaders this template uses - RFC 8594 — The Sunset HTTP Header Field: the registered
Sunsetheader semantics - GraphQL spec — @deprecated: schema-level deprecation for GraphQL fields
- Stripe API versioning: how a major public API handles dated, opt-in version changes
- API Changelog Template: the running log that pairs with every deprecation notice
- API Lifecycle Management Template: where deprecation sits in the full API lifecycle
Companion code: API deprecation notice resources: the template, middleware, and tracking queries from this page.
Frequently Asked Questions
How much notice should I give?
Public APIs: 90-180 days. Internal APIs: 30-60 days. Security-related changes: as fast as possible with direct outreach.
Should I support both versions indefinitely?
No. Every version you keep alive is more code to patch, more surface to secure, and more docs to keep honest. Pick a firm sunset date and hold it, with limited exceptions.
What HTTP status code should a deprecated endpoint return after sunset?
Return 410 Gone — it says 'this is gone forever' instead of 'this might be a bug'. Point consumers to the replacement in the body or a Link header so they aren't left guessing.
What if a major customer cannot migrate in time?
Offer a temporary extension with a documented expiration date. Track the extension in your deprecation log. Don't extend indefinitely — that defeats the purpose of the sunset.
Should I return warnings during the deprecation period?
Yes. Return 299 Miscellaneous Persistent Warning with a deprecation message in the Warning header. This is a soft signal that doesn't break clients but appears in logs.
How do I deprecate a GraphQL field?
Use the @deprecated directive in your schema:
type Order {
total: Float @deprecated(reason: "Use totalAmount instead. Removed in 2026-10-01.")
totalAmount: Float
}
GraphQL clients see the warning right in their introspection results, which is why schema-level deprecation beats a notice for GraphQL.
Should I version the entire API or just the changed endpoints?
Version the endpoint when one endpoint changes; bump the whole API only when a coordinated change hits most endpoints at once. Full version bumps are expensive for everyone, so don't spend one on a rename.
How do I handle deprecation in a webhook-based integration?
Send a deprecation.notice webhook event to every subscribed endpoint carrying the same payload as the notice — what changed, the sunset date, the migration link. Then send reminder events at T-30 and T-7, because webhook consumers ignore email too.
Related Resources
API Lifecycle Management Template
A copy-paste checklist template for managing API deprecation, versioning transitions, and safe sunset procedures.
DocMicroservice Contract Template
A template for defining service contracts and API agreements between microservices.
DocTechnical Specification Template
Use this technical specification template to document requirements, design decisions, architecture, API contracts, and acceptance criteria.
DocRollout Communication Template
A template for release notes and stakeholder updates during capability rollouts.
DocAPI Changelog Template
A template for documenting API changes including breaking changes, new features, deprecations, and bug fixes.
DocAPI Error Response Template
A reusable template for consistent, informative, and developer-friendly API error responses that reduce debugging time.