You deprecate GET /v1/invoices with ninety days of notice and an entry in the CHANGELOG. On the
sunset date a quarter of the keys that were calling it are still calling it, and their owners open
tickets saying they were never told. The entry sat in a repository that none of them read.
What you get
You will end up with separate RSS feeds for changes and campaigns, a three-notice plan for a ninety-day window, and a channel score computed from your request logs. This is for you if your deprecations reach the people they affect after the date has passed.
Short answer
Publish changes on a changelog page with an RSS feed of their own, kept apart from marketing, and email each breaking change to the owners of the API keys it affects. Add a Discord announcements channel or an in-product widget where your developers already look. Send three notices across a ninety-day window, and score the channels by how many affected keys still call the old shape on the sunset date.
You will need
A CHANGELOG for your API or SDK and a deprecation policy with a Sunset date. Writing both is covered elsewhere; this page is distribution only. Request logs that record the API key and the path of each call, because the score is computed from them. Node 22 or later for the sample, which reads a fixture of 1,708 log lines for fourteen keys.
Approaches compared
| Approach | When it fits | What it costs you | When to pick something else |
|---|---|---|---|
| Beamer | Your developers work inside your dashboard, and you can segment them by plan or by key | A script in your product and a vendor in the path of every page load | Your users call the API and never open your dashboard |
| Changelog page with RSS feed | Always, as the record every other channel links to | Nothing reaches anyone who did not subscribe, so it records rather than notifies | Never as the only channel for a breaking change |
| Discord announcements channel | You already run a Discord where your developers talk | Reaches the people in your server, who are rarely the owners of production keys | Your community is on a forum, or you have none |
| Buttondown or Mailchimp email digest | Breaking changes, sent to the owners of the keys they affect | Every unwanted email trains the reader to ignore the next one | The change affects nobody’s code, which makes it a feed item and not an email |
| LaunchNotes | One tool for the page, email, Slack, the widget, and RSS, with audience segments | A subscription, and the source of truth for the changelog moves into a vendor | You have a page and a mailing tool already |
The split is between pull and push. A page and its feed reach only the readers who come looking, so they cost nothing in annoyance and reach the fewest people. A widget and email reach people who were not looking, which is the point for a breaking change, and each needless message spends the attention the next one needs. Discord sits between the two: it pushes, but only to people in your server.
Keep changes and campaigns in separate feeds
Give changes a feed of their own and keep webinars, launches, and offers out of it. A developer
who mutes a feed for its marketing mutes the deprecation along with it. feed.mjs builds one
RSS 2.0 feed per kind from a single list, and refuses
an entry with no kind rather than guessing which feed it belongs in. It also refuses an id used
twice, because the id is the guid, which a reader may use to decide whether an item is new.
export function buildFeed(entries, kind) {
entries.forEach(checkEntry)
// The id is the guid, which the RSS specification lets an aggregator use to decide whether an
// item is new, and leaves the feed to keep unique. A reminder copied from the announcement with
// its id unchanged can be skipped as already seen.
const ids = new Set()
for (const e of entries) {
if (ids.has(e.id)) throw new Error(`entry ${e.id}: id used twice, so a reader may show only one of them`)
ids.add(e.id)
}
const feed = FEEDS[kind]
if (!feed) throw new Error(`no feed for kind "${kind}"`)
const items = entries
.filter((e) => e.kind === kind)
.sort((a, b) => Date.parse(b.date) - Date.parse(a.date))
Build the changes feed. The first item is the reminder for the invoices sunset:
node feed.mjs change
<item>
<title>30 days left: GET /v1/invoices stops answering on 2026-09-30</title>
<link>https://api.example.com/changelog/2026-08-31-invoices-v1-reminder</link>
<guid isPermaLink="false">2026-08-31-invoices-v1-reminder</guid>
<pubDate>Mon, 31 Aug 2026 09:00:00 GMT</pubDate>
<category>breaking</category>
<category>deprecation</category>
<description>Keys still calling /v1/invoices have been emailed directly. The migration guide has the field mapping & the cursor loop.</description>
The guid is the entry’s id, not its URL, so moving the changelog to another domain does not make
every reader see every item again. The pubDate is in the RFC 822 format the specification
requires.1 The summary’s & is escaped, and a control character pasted from a log line makes the
build fail rather than ship a feed every reader rejects.
Push to the owners of affected keys
The feed is the source; the email is what reaches people. Both mailing tools read a feed. The RSS-to-email feature in Buttondown checks the feed every thirty minutes and can send per item, weekly, or monthly. The RSS automation in Mailchimp sends daily, weekly, or monthly. Point either at the changes feed, never at the announcements feed.
A digest to every subscriber is the wrong audience for a breaking change. The people who need the email are the owners of keys that called the old shape, and your logs name them. Load those owners into the mailing tool as a list or segment of their own before the first notice, and send the reminders to that audience only.
Discord’s announcement channels are the
GUILD_ANNOUNCEMENT type, which other
servers can follow, so a customer’s team can pipe your notices into its own server. Beamer and
LaunchNotes put the same item in front of a signed-in user as a widget or banner.
Send three notices in ninety days
Each notice says what changes and gives the exact date and time in UTC. It says what to do, with a link to the migration guide, and what the old shape will answer once the date passes. The second and third notices go only to owners whose keys are still calling.
node channel.mjs
sunset 2026-09-30, 1708 calls read, 0 lines skipped
T-90 2026-07-02 announce 12 keys still on /v1 that day; page and RSS, email to every affected owner, Discord, widget
T-30 2026-08-31 reminder 7 keys still on /v1 that day; page and RSS, email to owners of keys still calling
T-7 2026-09-23 final 5 keys still on /v1 that day; email to owners of keys still calling, widget banner
The count beside each notice is the size of the audience for that notice. A key counts as still on
/v1 when the last day it called either version included a call to /v1. A key that runs a
weekly batch stays on the list between its calls instead of dropping off on a day it happened not to
run. The count shrinks because people moved, and the shrinking list is the email list.
Put the dates on the response as well. The
Deprecation header and the
Sunset header on every /v1 response reach the code
when the people have not read anything. A client library can log a warning on the first one it
sees.2
Score the channels on the sunset date
The score is one number: of the keys that called the old shape in the 30 days before the first notice, how many were still on it on the sunset date. It uses the same last-call rule. The same run prints it, and refuses to score a log in which no key called the old shape, which is a parsing problem rather than a perfect result:
node channel.mjs
affected keys (called /v1 from 2026-06-02 to the day before 2026-07-02): 12
channel score: 3 of 12 still on /v1 on 2026-09-30 (key_09, key_10, key_12)
email 6 affected keys, 1 still calling
rss 3 affected keys, 1 still calling
discord 2 affected keys, 0 still calling
no channel 2 affected keys, 1 still calling
widget 2 affected keys, 0 still calling
Lower is better, and zero is the target. Read the rows as leads, not results: twelve keys is a
fixture, and a key on two channels counts in both rows. The no channel row is the one to act on,
because no notice could reach those owners at all.
Check it worked
Run the tests. Twelve cases pin the notice dates, the score, keys that start calling after the first notice, keys that migrate and go back, log lines that do not parse, and the feeds themselves. Three of them cover a key that calls once a week, a log the parser cannot read, and an id used twice.
node --test
1..12
# tests 12
# suites 0
# pass 12
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 187.551225
Two of them parse each feed with fast-xml-parser, so a feed that would break a reader fails here
first. The line to check in your own run is channel score, compared with your last deprecation.
When it goes wrong
The first email arrives without the deprecation. The first RSS send from Mailchimp includes only posts from the last 24 hours, seven days, or 30 days, depending on the schedule. An automation set up a fortnight after a notice on a weekly schedule never mails it. Set up the automation before the first notice, or send that notice by hand.
A key whose owner was emailed three times still calls /v1 on the sunset date, as key_12 does in
the fixture. An address on a key can outlive its owner’s job. Check the bounce and reply reports
after the first notice, and find a current owner while sixty days remain.
Readers unsubscribe from the changes feed because it carries campaigns. Split the feed, and announce the split in the feed they still have.
When not to do this
Do not email every subscriber about every change. A digest of additive changes trains readers to archive the next email unread, including the one that carries the breaking change.
Do not count opens as reach. A notice that was opened and not acted on still leaves a key calling the old shape, which is what the score measures.
Do not run a widget for an API whose users never open your dashboard. A message in a product they do not visit reaches nobody.
Do not move the sunset date because the score is bad on day 83. Move it only if the notices went out late, and send a fresh first notice when you do.
Related how-tos
Last verified
Verified 2026-10-15 against Node 22.22.0 and fast-xml-parser 5.11.2. Every output block is what
the command preceding it printed. The log, the owners, and the channels are a fixture for a
fictional API, scored offline without sending email or calling a vendor’s service. Vendor behavior
is quoted from each vendor’s documentation as read on that date.
Footnotes
-
The RSS 2.0 specification says all its dates conform to RFC 822, with the exception that the year may be two characters or four, four preferred. RFC 822, from 1982, gives the year as
2DIGITand offers20 Jun 82as its example. Taken on its own, the standard a feed cites could not tell 2026 from 1926. ↩︎ Back to text -
The two headers do not agree on how to write a date. RFC 9745 says that for historical reasons the Sunset field uses a different format, and its example writes the Sunset date ending in
UTC. RFC 9110 defines an HTTP date as ending in the literalGMT, which leaves the example outside the grammar it inherits. ↩︎ Back to text