Direct answer: A deprecation notice developers actually act on needs three things working together — a machine-readable Deprecation and Sunset header pair (RFC 9745 and RFC 8594) on every response, a Link header pointing straight to a migration guide, and a sunset date set at least 3–6 months out. Skip any one of these and the notice gets ignored until the version actually breaks.
Most deprecation notices die in a changelog nobody reads. You ship v2, you write a blog post announcing it, maybe you send an email. Six months later v1 is still getting 40% of your traffic and someone on your team is asking why nobody migrated.
They didn’t migrate because nothing forced them to notice. A blog post is not a notice. It’s an announcement, and announcements get buried in inboxes the same day they arrive.
Why deprecation notices get ignored
Developers don’t read your blog. They read their terminal output, their error logs, and — if you’re lucky — the response headers coming back from your API. If the deprecation warning isn’t sitting in one of those three places, it doesn’t exist as far as their workflow is concerned.
This is the part teams get wrong most often: they treat deprecation as a documentation problem when it’s actually a plumbing problem. The notice has to travel with the API response itself, not live in a separate system the developer has to go check.
The three headers that actually do the work
There are two IETF-standardized headers for this now, plus one that ties them together.
Deprecation (RFC 9745) — tells the client this version is deprecated and, optionally, when that took effect.
Sunset (RFC 8594) — tells the client the actual date the version stops working. This is the one that should scare people into action.
Link (RFC 8288, rel="deprecation") — points to the actual migration doc, not your homepage.
Here’s what that looks like on a real response:
HTTP/1.1 200 OK
Content-Type: application/json
Deprecation: @1704067200
Sunset: Sat, 31 Jan 2026 23:59:59 GMT
Link: <https://api.yourcompany.com/docs/migrate-v1-to-v2>; rel="deprecation"; type="text/html"
The Deprecation value is a Unix timestamp (prefixed with @) marking when the version was deprecated — not when it dies. Sunset uses a standard HTTP-date for the actual shutoff. Two different dates, two different jobs. A lot of teams collapse these into one header and lose the “you still have time” signal entirely.
One thing that trips people up: don’t hand-write these in every route handler. You’ll forget one, someone will ship a fix without the header, and now you’ve got an inconsistency that undermines trust in the whole system. Add it as middleware, keyed off the API version in the route, so it’s structurally impossible to skip.
Setting a sunset date that doesn’t get you yelled at
Three months is the bare minimum, and honestly it’s tight if any of your callers are enterprise clients with their own release cycles and change-approval processes. Six months is safer. If you have SDKs in multiple languages, add another month or two — someone’s always waiting on a downstream package update.
The date isn’t just a courtesy. Once Sunset is in the past, you’re within your rights to actually turn the version off. Set it too aggressively and you’ll end up quietly extending it anyway, which teaches your API consumers that your deprecation dates are more like suggestions. That reputation is hard to undo.
What the migration guide actually needs to say
This is where most deprecation notices fall apart even when the headers are perfect. The Link header sends someone to a doc, and the doc is a wall of prose explaining why you made the change. Nobody asked why. They want to know what to change in their code.
Structure it like this instead:
- One-line summary — what changed, in a sentence a developer can screenshot into Slack.
- Side-by-side request/response diff — old shape next to new shape, not described in paragraphs.
- The actual breaking changes, as a list — renamed fields, changed types, removed endpoints. Nothing else belongs in this list.
- A find-and-replace path, if one exists. If migrating is genuinely just swapping
/v1/usersfor/v2/users, say that in the first three lines, not buried in paragraph four. - A working code sample in at least two languages — whatever your top two SDK languages are by usage.
- Where to get help — a real channel, not a generic “contact support” line.
If you already have an OpenAPI spec for the new version (worth reading alongside this if you haven’t — How to Write OpenAPI 3.1 Specs That Pass Validation), link straight to it from the migration guide. Don’t make someone re-derive the schema from your prose description of it.
Surface the notice more than once
Headers cover the developers actually hitting your API. They don’t cover the ones who set up an integration eighteen months ago and haven’t touched it since — the ones most likely to get blindsided.
For that group, you need redundancy:
- A banner in your API dashboard, if you have one, tied to the specific keys/tokens still calling the old version.
- An email at three checkpoints: when deprecation starts, at the halfway mark, and two weeks before sunset. Not just once.
- A line in your changelog (worth pairing this post with one on writing changelogs people trust, since the same “will anyone actually read this” problem shows up there too).
- If you can pull it off, a direct email to whoever’s account is still generating traffic against the old version. Nothing beats “hey, we can see you’re still calling v1” for getting a response.
A minimal deprecation notice template
## [API Name] v1 is deprecated — migrate to v2 by [date]
**What's changing:** [one sentence]
**Deprecated as of:** [date]
**Sunset date:** [date — hard cutoff]
**Why:** [one or two sentences, not a paragraph]
### What breaks
- [field/endpoint]: [old] → [new]
- [field/endpoint]: [old] → [new]
### Migration
[code sample, before/after]
### Need help?
[real contact path]
Keep it this short. The temptation is to add context, justify the decision, apologize for the inconvenience. Cut all of it. The developer reading this at 11pm because their integration just started throwing warnings doesn’t want your reasoning — they want to fix it and go back to bed.
FAQ
Do I need both the Deprecation and Sunset headers, or is one enough?
Use both. Deprecation tells the client the version is no longer preferred; Sunset tells them exactly when it stops working. Without Sunset, there’s no forcing function — deprecated-but-still-working can drag on for years.
What HTTP status code should a deprecated endpoint return?
Still 200 OK (or whatever it normally returns) until the sunset date passes. Don’t downgrade behavior before the date you promised — that breaks trust faster than the deprecation itself. After sunset, 410 Gone is the correct response, not a 404.
Should I version APIs in the URL path or with headers?
Path versioning (/v2/users) is more visible and easier for developers to reason about, which matters more here than the “purity” arguments for header-based versioning. Most of your audience will notice a URL change in code review; almost nobody notices a header change.
How early should the Sunset header appear before the actual cutoff?
As soon as deprecation is announced — not just in the final weeks. The whole point of Sunset is giving people a countdown they can plan around.
What if a client ignores every warning and the sunset date arrives anyway?
Turn it off. If you don’t, every future deprecation notice you send becomes background noise, because you’ve already demonstrated the dates aren’t real.
Can I automate deprecation header injection instead of adding it per-route?
Yes, and you should. Middleware keyed to API version is far more reliable than trusting every route handler to remember it. This is also where testing the headers in CI earns its keep — treat a missing Sunset header on a deprecated route as a build failure.