Release notes: how to write them so your docs doesn't rot

Oluwatise Okuwobi

Content Marketing Manager

Your API shipped a change last Tuesday. The release note went out that morning: customer_id is now required on POST /payments, effective immediately. Clear, dated, accurate.

A developer reads it, clicks through to the reference page, and finds customer_id listed as optional. The quickstart doesn't mention it at all.

They probably won't file a ticket about the contradiction. They'll stop trusting both pages and start testing against your sandbox to find out what's actually true.

Release notes are supposed to prevent this. Most of them cause it, because they get written as an announcement about the release instead of a record of what the documentation now has to say.

Here's the format, a template you can copy, examples worth stealing, and the mistake that makes the rest of it pointless.

What are release notes?

Release notes tell the people who use your product what changed, why it matters to them, and what they need to do about it. That third part is the whole job. A log entry says customer_id is now required. A release note says it's required, integrations that don't send it will start failing on Monday, and here's the two line fix.

Three documents get conflated here, and they have different readers.


Commit log

Changelog

Release notes

Written for

Your team

Developers tracking the project

People using the product

Granularity

Every change

Every user visible change

Changes that need a decision or an action

Answers

What was changed

What changed, and when

What changed, why it matters, what to do now

Usually written by

Whoever committed

Generated, then edited

Whoever owns the docs

The release notes versus changelog question comes up constantly, and automation is why. Tooling that builds a page from merged pull requests gives you a changelog, often a good one. It cannot tell a developer that a change breaks their integration, because that judgment was never in the commit message.

Plenty of teams publish one page and call it both. That holds up while the product is small and every change is additive. It falls apart the first time you ship something breaking, because a developer scanning fourteen bullets has no way to spot the one that costs them a weekend.

Which of the three you publish matters far less than what goes in it.

What to include in release notes

A release note needs six things: a version and date, a summary in plain language, changes grouped by type, breaking changes called out separately, a migration path for anything that breaks, and links to the reference pages that changed. Skip the last two and you've written an announcement.

  1. The version number and date. Boring, non-negotiable, and missing more often than you'd expect. A developer debugging a failed integration needs to know whether your change or their deploy landed first.

  2. A one line summary in plain language. Your support lead should be able to read it and know what's about to hit the queue.

  3. Changes grouped by type: added, changed, deprecated, fixed, security. Borrow those categories from Keep a Changelog instead of inventing your own, because developers already recognize them.

  4. Breaking changes, in a block of their own.

  5. A migration path for anything breaking or deprecated. One sentence and a link, minimum.

  6. Links to every reference page that changed.

That's the release notes format. Order matters less than completeness, with one exception.

Breaking changes go in their own block, above the grouped list. Never as a bullet at position nine. Developers read release notes the way they read stack traces, scanning for the part that affects them and skipping the rest. If the change that will take down their integration is formatted identically to a copy fix in the dashboard, you've hidden it in plain sight, and you will hear about it in support.

Item six is the one almost everyone skips, and it decides whether the note does any work at all. A release note that describes a change without linking to the page documenting it is asking the reader to go find that page, trust that it's current, and reconcile the difference themselves. Most won't bother. A release note with no links into the docs is a press release with a version number on it.

What to leave out

Internal ticket IDs that resolve to a board nobody outside your company can open. Refactors with no user visible effect. Dependency bumps, unless one changes a minimum version your users have to match.

And "various bug fixes and improvements." Name the fix or cut the line. That phrase tells a developer who has spent two days chasing a bug that you might have fixed it, or might not, and the only way to find out is to test it themselves. It's one of the most common lines in software release notes and it has never helped anyone.

A release notes template you can copy

Use two templates, not one. API changes and product changes reach different readers with different consequences, and forcing both into a single format means either the endpoint detail buries the product update or the friendly summary hides the breaking change.

API release note template

## v2.4.0 (2026-08-11)

One line on what this release changes for integrators.

### Breaking changes

**`customer_id` is now required on `POST /payments`.**
Requests without it return `400 invalid_request` starting 2026-09-15.
To migrate, add `customer_id` to your payment payload.
See [Create a payment](/reference/payments/create).

### Added
- `GET /payments/{id}/attempts` returns retry history for a payment.
  [Reference](/reference/payments/attempts)

### Changed
- `POST /refunds` accepts partial amounts. Omitting `amount` still
  refunds in full, so existing calls are unaffected.
  [Reference](/reference/refunds/create)

### Deprecated
- `POST /charges` is deprecated in favor of `POST /payments`. It keeps
  working until 2027-02-01. [Migration guide](/guides/charges-to-payments)

## v2.4.0 (2026-08-11)

One line on what this release changes for integrators.

### Breaking changes

**`customer_id` is now required on `POST /payments`.**
Requests without it return `400 invalid_request` starting 2026-09-15.
To migrate, add `customer_id` to your payment payload.
See [Create a payment](/reference/payments/create).

### Added
- `GET /payments/{id}/attempts` returns retry history for a payment.
  [Reference](/reference/payments/attempts)

### Changed
- `POST /refunds` accepts partial amounts. Omitting `amount` still
  refunds in full, so existing calls are unaffected.
  [Reference](/reference/refunds/create)

### Deprecated
- `POST /charges` is deprecated in favor of `POST /payments`. It keeps
  working until 2027-02-01. [Migration guide](/guides/charges-to-payments)

## v2.4.0 (2026-08-11)

One line on what this release changes for integrators.

### Breaking changes

**`customer_id` is now required on `POST /payments`.**
Requests without it return `400 invalid_request` starting 2026-09-15.
To migrate, add `customer_id` to your payment payload.
See [Create a payment](/reference/payments/create).

### Added
- `GET /payments/{id}/attempts` returns retry history for a payment.
  [Reference](/reference/payments/attempts)

### Changed
- `POST /refunds` accepts partial amounts. Omitting `amount` still
  refunds in full, so existing calls are unaffected.
  [Reference](/reference/refunds/create)

### Deprecated
- `POST /charges` is deprecated in favor of `POST /payments`. It keeps
  working until 2027-02-01. [Migration guide](/guides/charges-to-payments)

## v2.4.0 (2026-08-11)

One line on what this release changes for integrators.

### Breaking changes

**`customer_id` is now required on `POST /payments`.**
Requests without it return `400 invalid_request` starting 2026-09-15.
To migrate, add `customer_id` to your payment payload.
See [Create a payment](/reference/payments/create).

### Added
- `GET /payments/{id}/attempts` returns retry history for a payment.
  [Reference](/reference/payments/attempts)

### Changed
- `POST /refunds` accepts partial amounts. Omitting `amount` still
  refunds in full, so existing calls are unaffected.
  [Reference](/reference/refunds/create)

### Deprecated
- `POST /charges` is deprecated in favor of `POST /payments`. It keeps
  working until 2027-02-01. [Migration guide](/guides/charges-to-payments)

Every entry links to the page that documents it. That's the part to keep when you adapt this.

Product release note template

## [Release name] (YYYY-MM-DD)

One line on what changed and who it affects.

### What's new
- [Feature]. What it does and where to find it. [Docs](/docs/feature)

### What changed
- [Old behavior] is now [new behavior]. If you [did X], you'll
  need to [do Y]. [Docs](/docs/thing)

### Fixed
- [The specific bug], not "various issues."

### Known issues
- [What's still broken]

## [Release name] (YYYY-MM-DD)

One line on what changed and who it affects.

### What's new
- [Feature]. What it does and where to find it. [Docs](/docs/feature)

### What changed
- [Old behavior] is now [new behavior]. If you [did X], you'll
  need to [do Y]. [Docs](/docs/thing)

### Fixed
- [The specific bug], not "various issues."

### Known issues
- [What's still broken]

## [Release name] (YYYY-MM-DD)

One line on what changed and who it affects.

### What's new
- [Feature]. What it does and where to find it. [Docs](/docs/feature)

### What changed
- [Old behavior] is now [new behavior]. If you [did X], you'll
  need to [do Y]. [Docs](/docs/thing)

### Fixed
- [The specific bug], not "various issues."

### Known issues
- [What's still broken]

## [Release name] (YYYY-MM-DD)

One line on what changed and who it affects.

### What's new
- [Feature]. What it does and where to find it. [Docs](/docs/feature)

### What changed
- [Old behavior] is now [new behavior]. If you [did X], you'll
  need to [do Y]. [Docs](/docs/thing)

### Fixed
- [The specific bug], not "various issues."

### Known issues
- [What's still broken]

The known issues block is the one teams cut first and shouldn't. Publishing a bug you haven't fixed yet costs you nothing with the developer who already hit it, and saves you the ticket from everyone who is about to.

Both templates are plain markdown, so they paste into GitHub releases, Confluence, Notion, or a Word doc without reformatting. Keep the file in the repo next to the code rather than in someone's notes app. Once it lives there, your pull request template can point at it, and writing the note becomes part of shipping instead of something a person remembers to do afterward.

Release notes examples worth stealing from

Three teams, each solving a different part of the problem. Every one of them made a deliberate decision most teams never get around to making. The visual design is the least interesting thing about any of them.

Stripe: breaking is a field, not a writing style

Stripe versions its API by date and codename, like 2026-07-29.dahlia, and lists changes in tables with a column headed "Breaking change?" Every row reads either Breaking or Non-breaking. Nothing depends on interpretation, or on how emphatically someone wrote the bullet. Breaking changes also land in preview releases first and graduate to stable later, so integrators see what's coming before it reaches them [External Link: Stripe API changelog].

Steal this one. Once "does this break something" is a property of the entry rather than a matter of tone, it stops depending on whoever is writing the note at 6pm on a Thursday.

Twilio: let people filter to what they actually use

Twilio's changelog spans more than 40 products and lets readers narrow it to the ones they run. Entries link out to the relevant API docs, and anything requiring action states the action. An August 2026 entry announcing a new regional IP address for Event Streams webhooks named the allowlisting step directly instead of leaving readers to deduce it .

Steal this if you ship more than one product. An unfiltered feed asks every reader to sort your catalog on your behalf, and most of them won't structuring docs for multiple products

Linear: match the format to the reader

Linear writes to the product template rather than the API one. Dated headers, a short narrative on what the feature does, video or images for anything visual, and a fixes and improvements list underneath, grouped by area. The tone is conversational and the focus is on what someone can now do Linear changelog.

Steal the discipline behind it. Linear's readers aren't integrating endpoints, so a table of parameter changes would be the wrong artifact. Format follows audience.

And the ones to avoid

A few patterns turn up over and over in the release notes we audit.

The wall: nineteen bullets, no grouping, no ranking, and the breaking change sitting at position twelve in the same typeface as a tooltip fix.

The stub: a version number, a date, and "various improvements and bug fixes." Technically a release note. Functionally a timestamp.

The orphan: well written, properly grouped, breaking changes flagged, and not one link into the reference docs. The reader learns a field is now required, then goes hunting for the page that says so.

That last one is the most common, and it's worth sitting with, because it looks like a good release note right up until somebody follows it.

The mistake that makes all of this worthless

A release note is a public, timestamped claim about how your product behaves. When the reference page, the quickstart, and the SDK examples don't move at the same time, that claim turns into the most precise evidence anyone has that your documentation is out of date.

That's the trap in one line. A well written release note makes drift legible and puts a date on it.

The failure runs the same way every time. Someone ships the change. Someone writes the note, usually well, often the same day.

The reference page goes untouched, because updating it belongs to a different person, a different sprint, or to nobody in particular. A developer reads the note, follows it through to the page, and finds two versions of the truth with your name on both.

Now they have a choice and no good version of it: believe the note, believe the page, or go test it against the sandbox. Most test it, which is slower than either. Some open a ticket. A few decide your docs can't be relied on and stop opening them, which is the expensive outcome, because every question they have from then on becomes a question for your engineers.

We've audited portals where more than 40% of code examples failed against the current API version the changelog was usually fine.

There's a structural reason for that, and it's the part most teams miss. A changelog is append only. Keeping it current means adding a line, and nobody ever has to go back and correct anything.

Reference documentation is edit in place. Keeping it current means hunting down every page that described the old behavior and changing each one. One of those jobs survives a busy release week. The other doesn't, which is why the two drift apart in exactly one direction.

The fix is smaller than it sounds and it's a question of order. Write the release note from the diff, before you ship.

Written afterward, a note records what somebody remembers changing. Written from the diff first, it forces a person to enumerate every user visible change and then ask, for each one, which page is now wrong. That question is the whole job.

Ask it before the release and the docs and the note go out together. Ask it after and you've published a dated record of your own backlog.

Common order:  ship  ->  announce  ->  update the docs when someone complains
Better order:  enumerate the diff  ->  update docs and note together  ->  ship
Common order:  ship  ->  announce  ->  update the docs when someone complains
Better order:  enumerate the diff  ->  update docs and note together  ->  ship
Common order:  ship  ->  announce  ->  update the docs when someone complains
Better order:  enumerate the diff  ->  update docs and note together  ->  ship
Common order:  ship  ->  announce  ->  update the docs when someone complains
Better order:  enumerate the diff  ->  update docs and note together  ->  ship

This isn't a writing problem, which is why a better template won't solve it. It comes down to who is accountable for the second half.

Who should write release notes?

Whoever is accountable for the documentation being correct after the release ships. Not whoever has the most context on the change, and not whoever writes the cleanest prose. On most teams that person doesn't exist, which is the real reason release notes drift away from the pages they point at.

Three setups are common, and each fails somewhere predictable.

The engineer who shipped it writes fast and accurately, and often produces something nobody outside the team can use. They describe the implementation instead of the consequence. "Refactored the payment intent state machine" is entirely true and tells an integrator nothing about whether their code still runs on Monday.

The product manager writes something readable and misses the technical consequence. They'll cover the new capability well and skip the response field that changed type, because nobody flagged it as user facing.

The support team would write the best notes of the three, since they know precisely which confusions cost them hours, and they find out last. By the time support hears about a change it has already shipped, and the note becomes archaeology.

What works is one person accountable for the note and for the pages it links to, with the engineers as sources rather than authors. The job title matters less than the pairing. When the same person owns both halves, the note cannot ship ahead of the docs, because they're one piece of work instead of two.

That's easier to describe than to staff. In practice it usually looks like one writer chasing half the engineering team for context on release day, every release day. It holds up as long as somebody is willing to be mildly annoying about it forever.

The teams where this breaks rarely chose the wrong process. They just quietly stopped doing it during a quarter when everything was on fire.

This is where release notes tooling disappoints the teams that buy it. A widget that publishes a formatted feed into your app solves distribution, and distribution was never the thing that was broken. The failure is that nobody's job description contains the sentence "the reference docs are correct on release day."

How to automate release notes without making them useless

Automation is good at the mechanical half: version numbers, dates, collecting merged pull requests, grouping them by label, publishing on a schedule. Give it all of that. It's the work humans do badly and resent doing.

What it can't do is the part that makes a release note worth reading. No tool can tell you whether a change breaks an existing integration, what the reader has to do about it, or which reference page is now wrong. That information was never in the commit history.

A pull request titled "make customer_id required" states a fact. Whether it costs your integrators a weekend depends on how many of them already send that field, and nothing in your repo knows the answer.

So auto-generated notes give you a changelog. A useful one, worth publishing, and not what a developer needs on the day you ship something breaking.

The workable middle ground is to structure the inputs so the generator produces a first draft a person can finish.

  • Conventional Commits assigns every commit a type and an optional breaking-change marker, so the generator can group changes and flag the dangerous ones without guessing.

  • Keep a Changelog gives you categories nobody has to renegotiate each release.

  • A single checkbox in your pull request template asking "does this change any documented behavior, and which page?" costs the author about fifteen seconds.

That last one returns more than the other two combined, and it's free. It captures the only input no tool can infer, at the moment the person who knows the answer is already looking at the change.

Then let the generator draft. A human decides what's breaking, writes the migration path, and checks that the pages the note links to now say the same thing the note says. That check runs a few minutes per release, and it's the whole difference between a note that helps and a note that timestamps your drift.

The short version

  • A release note answers what changed, why it matters, and what the reader does now. Drop the third and you've written a commit log with nicer formatting.

  • Breaking changes get their own block, above the list, never a bullet buried inside it.

  • Every entry links to the reference page that documents it. Almost nobody does this, and it decides whether the note is worth publishing.

  • Automation can draft the note. It can't judge what's breaking, write the migration path, or notice that your reference docs now contradict you.

  • Release notes written after the release record your drift. Written from the diff before the release, they catch it.

If the drift is the part that sounds familiar, [Internal Link: what bad documentation actually costs → /blog/cost-of-bad-api-documentation] puts numbers against it, and [Internal Link: auditing your API documentation → /blog/api-documentation-audit-quality-cycle] covers how to find the pages that are already wrong. If you couldn't name the person who owns the second half of a release, that's the usual answer, and it's worth sorting out before you worry about the template. Keeping documentation accurate through every release is the work we take on documentation ownership.