How-to › Document and support developers

How to run a changelog channel developers read in time#

Carry breaking changes to developers by feed, email, chat, and widget, then score the channels by the keys still calling the old shape on the sunset date.

Audience
API producer
Level
intermediate
Topic
Run developer relations
Verified

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

ApproachWhen it fitsWhat it costs youWhen to pick something else
BeamerYour developers work inside your dashboard, and you can segment them by plan or by keyA script in your product and a vendor in the path of every page loadYour users call the API and never open your dashboard
Changelog page with RSS feedAlways, as the record every other channel links toNothing reaches anyone who did not subscribe, so it records rather than notifiesNever as the only channel for a breaking change
Discord announcements channelYou already run a Discord where your developers talkReaches the people in your server, who are rarely the owners of production keysYour community is on a forum, or you have none
Buttondown or Mailchimp email digestBreaking changes, sent to the owners of the keys they affectEvery unwanted email trains the reader to ignore the next oneThe change affects nobody’s code, which makes it a feed item and not an email
LaunchNotesOne tool for the page, email, Slack, the widget, and RSS, with audience segmentsA subscription, and the source of truth for the changelog moves into a vendorYou 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 &amp; 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.

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

  1. 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 2DIGIT and offers 20 Jun 82 as its example. Taken on its own, the standard a feed cites could not tell 2026 from 1926. ↩︎ Back to text

  2. 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 literal GMT, which leaves the example outside the grammar it inherits. ↩︎ Back to text

Read this page as markdown · All how-to guides

Generate the client instead of writing it#

Retries, timeouts, pagination and auth are the same problems in every client. Voxgig generates them from your OpenAPI description, in 23 languages, from one model.

Listen to Fireside, the Voxgig podcast

Since 2018, Richard Rodger has talked with developer advocates, event organizers, and the people who build developer-facing companies about the specifics of the work.