Deprecating an API without breaking clients: Deprecation and Sunset headers, successor-version, and OpenAPI
Most "deprecations" are either silent deletions that break an integration at 2 a.m., or permanent warnings that glow for years until everyone ignores them. A real deprecation is a timeline with a signal clients can automate against, a pointer to the replacement, and a cut-off that actually happens. HTTP and OpenAPI already define the machinery; the missing piece is usually the discipline to use all of it together.
The four signals
| Signal | Where | What it says |
|---|---|---|
Deprecation header (RFC 8594) | Every response | "This endpoint is deprecated as of this HTTP-date." |
Sunset header (RFC 8594) | Every response | "It will stop working after this HTTP-date." |
Link: rel="successor-version" | Every response | "The replacement lives at this URL." |
deprecated: true + extensions | OpenAPI | Marks the operation in docs and generated SDKs, with the same dates |
The headers are the runtime signal that reaches code that never reads your changelog; the OpenAPI annotation is the design-time signal that reaches developers before they ship new calls. Use both, with matching dates.
The headers in action
HTTP/1.1 200 OK
Deprecation: Wed, 01 Oct 2025 00:00:00 GMT
Sunset: Wed, 01 Apr 2026 00:00:00 GMT
Link: <https://api.example.com/v2/charges>; rel="successor-version"
Content-Type: application/jsonDeprecationcarries the date the deprecation was announced (ortruewhen you do not want to state a date; prefer a date).Sunsetcarries the cut-off. The gap between the two is the migration window.- The
Linkheader points at the replacement resource or documentation; userel="successor-version"for a versioned successor andrel="alternate"for a guide.
Clients can log these headers in middleware and open tickets automatically, which is far more reliable than hoping a human saw the announcement.
Document it in OpenAPI
Mark the operation deprecated and carry the timeline and migration note in a consistent extension so renderers and SDKs can surface it:
paths:
/v1/charges:
get:
deprecated: true
summary: List charges (deprecated; use v2)
description: >-
Deprecated 2025-10-01 and scheduled for removal 2026-04-01.
Migrate to GET /v2/charges, which paginates with cursors and returns
amounts as minor-unit integers.
x-deprecation:
deprecated_at: '2025-10-01T00:00:00Z'
sunset_at: '2026-04-01T00:00:00Z'
successor: /v2/charges
guide: https://docs.example.com/migrations/v1-to-v2-charges
responses:
'200':
description: Charges (legacy shape).
headers:
Deprecation:
schema: { type: string, format: http-date }
Sunset:
schema: { type: string, format: http-date }
Link:
schema: { type: string }
content:
application/json:
schema:
$ref: '#/components/schemas/LegacyChargesResponse'Documenting the headers on the response makes them visible in generated docs and lets contract tests assert they are actually being sent. Many generators also surface deprecated: true as a strikethrough or compiler warning, which discourages new call sites.
Pick a realistic timeline and hold to it
A defensible window depends on who depends on the endpoint:
| Surface | Typical minimum window |
|---|---|
| Internal-only, few callers | 4 to 8 weeks |
| Public API, authenticated partners | 6 to 12 months |
| Public API, unauthenticated or widely embedded | 12 months or longer |
Announce, emit headers, and leave the endpoint fully functional during the window. Deprecation is not a slowdown or a behavior change; if you make the legacy path flaky to push migration, you break clients before the sunset and lose the trust that makes future deprecations credible.
Drive migration with telemetry, not guesses
You cannot retire what you cannot see. Before announcing, instrument the endpoint to answer: who calls it, how often, and which user-agent or credential. Then:
- At announcement, notify every active caller you can identify, with the successor and a guide.
- During the window, watch traffic; contact teams whose calls never decline.
- Near sunset, send a final reminder to the remaining callers.
- Keep a list of known stragglers so the cut-off is a decision, not a surprise.
If significant traffic remains at sunset with no owner, that is evidence the window or the migration path was wrong; extend once with a clear final date rather than silently deleting or silently keeping it forever.
What happens after sunset
Decide and document the post-sunset behavior in advance. The clean options are:
- Remove and return
404/410 Gone.410 Goneis the precise status for a deliberately removed resource; pair it with a JSON body linking the successor. - Redirect to the successor only when the call is mechanically translatable (for example a path rename with the same request and response), using
301for GET and documenting it; do not redirect when shapes differ, as a silent transform hides breakage. - Versioned error with a clear
typeand the migration guide, so an automated client can surface an actionable message.
Do not leave a half-working endpoint. Returning 200 with subtly wrong data after sunset is worse than a loud 410.
Deprecating fields and parameters
The same discipline applies below the endpoint level. A deprecated field or query parameter should:
- be marked
deprecated: trueon the schema property or parameter; - keep working through the window;
- be described with the replacement (for example
amountfloat replaced byamount_minorinteger); - be removed only with a version bump or a documented breaking window.
Removing a field is a breaking change even when the URL stays the same, so field-level deprecations belong on the same timeline and diff checks as endpoint removals.
What codegen, contract tests, and AI callers need
- Generators mark deprecated operations and properties so new code does not adopt them and IDEs warn on existing use.
- A contract test can assert that every operation with
deprecated: trueactually returnsDeprecationandSunsetheaders, closing the gap between the spec and the running API. - An OpenAPI diff in CI catches an unannounced removal before it ships, so deletions become scheduled sunsets rather than accidents.
- An AI agent or generated client reading the spec sees the successor pointer and can choose the current operation instead of building on the deprecated one.
Checklist
- Emit
Deprecation,Sunset, andLink rel="successor-version"on every deprecated response, with matching dates. - Mark the operation (and any deprecated fields)
deprecated: truein OpenAPI with a consistent extension carrying the timeline and migration guide. - Choose a window appropriate to the audience and keep the endpoint fully functional throughout.
- Instrument callers up front and drive migration by telemetry, contacting teams that do not move.
- Define post-sunset behavior now:
410 Gonewith a successor link, or a documented redirect only when shapes match. - Treat field and parameter removals as breaking changes on the same timeline.
- Add a contract test asserting the headers are sent and a CI diff that blocks unannounced removals.
Get these right and deprecation becomes a boring, predictable migration instead of a 2 a.m. outage and a damaged relationship with the developers who depend on you.
You can mark operations deprecated, generate SDKs that warn on them, and write contract tests asserting the sunset headers in one local-first workspace, right in your browser. For the versioning strategy that decides when a sunset is even necessary, see REST API versioning in 2026.