API Changelog Template
A template for documenting API changes including breaking changes, new features, deprecations, and bug fixes.
Overview
Consumers want three answers: what changed, when, and do they need to act. An unstructured changelog — or none — means they diff your API or find breaking changes in production. This template gives you a standard format: every entry versioned, dated, severity-marked, with a migration note. Companion docs: OpenAPI documentation for the spec itself, API logging for the operational side, and deprecation notices for the long tail.
When to Use
- For alternatives, see API Deprecation Notice Template.
Use this resource when:
- Releasing a new API version or capability
- Deprecating or removing an endpoint or field
- Fixing a bug that affects API behavior
- Publishing a monthly or quarterly API update
Solution
# API Changelog
## 2.5.0 — 2026-06-26
### Breaking Changes
- **Removed:** `GET /v1/orders/{id}/items` — Use `GET /v2/orders/{id}?expand=items` instead
- **Changed:** `total` field renamed to `totalAmount` in all order responses
- **Required:** `X-Request-ID` header now mandatory for all write operations
### New Capabilities
- **Added:** `POST /v2/orders/bulk` — Create up to 100 orders in a single request
- **Added:** `paymentStatus` field to order responses (`pending`, `paid`, `failed`)
- **Added:** Webhook event `order.payment_failed` for failed payment notifications
### Fixes
- **Fixed:** `GET /v2/products` now returns empty array `[]` instead of `null` when no products exist
- **Fixed:** Date fields now consistently return ISO 8601 format with timezone offset
### Deprecations
- **Deprecated:** `customer_id` query parameter — Use `customerId` instead (sunset: 2026-10-01)
- **Deprecated:** `v1` API base path — Migrate to `v2` before 2026-12-01
## 2.4.0 — 2026-05-15
### New Capabilities
- **Added:** `PATCH /v2/orders/{id}` for partial order updates
- **Added:** Rate limit headers (`X-RateLimit-Limit`, `X-RateLimit-Remaining`) to all responses
### Fixes
- **Fixed:** `500 Internal Server Error` on `POST /v2/orders` when `items` array was empty
---
## Versioning Policy
We follow [Semantic Versioning](https://semver.org/):
- **MAJOR:** Breaking changes that require consumer action
- **MINOR:** New capabilities, backward compatible
- **PATCH:** Bug fixes, backward compatible
## Change Categories
| Category | Description | Consumer Action Required |
|----------|-------------|--------------------------|
| Breaking Change | Existing integrations may fail | Yes — migration required |
| New Capability | New endpoints, fields, or behaviors | Optional — adopt when ready |
| Fix | Corrected bug or inconsistent behavior | No — but verify if you relied on old behavior |
| Deprecation | Capability scheduled for removal | Yes — before sunset date |
## Subscribe to Changes
- **RSS Feed:** https://developer.example.com/changelog.xml
- **Email:** Subscribe at https://developer.example.com/subscribe
- **Webhook:** Configure `api.changelog_published` event in your dashboard
- **Slack:** Join #api-announcements on our community Slack
Extended Template Example
Here is a more detailed changelog with several versions, showing how entries accumulate over time:
# API Changelog
## 3.0.0 — 2026-09-01
### Breaking Changes
- **Removed:** All v1 endpoints. Migrate to v2 or v3. See migration guide.
- **Changed:** Authentication switched from API keys to OAuth 2.0 bearer tokens
- **Changed:** Error response format standardized to RFC 7807 Problem Details
- **Removed:** `GET /v2/users/{id}/permissions` — Use `GET /v2/users/{id}?expand=permissions`
- **Changed:** Pagination `offset`/`limit` replaced with `cursor`/`limit`
### New Capabilities
- **Added:** `POST /v3/webhooks` — Programmatically register webhook endpoints
- **Added:** Bulk export endpoint `GET /v3/orders/export?format=csv`
- **Added:** Idempotency key support on all POST endpoints via `Idempotency-Key` header
### Fixes
- **Fixed:** Rate limit counters now reset correctly at midnight UTC instead of local server time
- **Fixed:** `PATCH /v2/orders/{id}` no longer overwrites `null` values when field is omitted
### Deprecations
- **Deprecated:** `v2` API base path — Migrate to `v3` before 2027-03-01
- **Deprecated:** `X-API-Key` header — Use `Authorization: Bearer <token>` instead
## 2.5.0 — 2026-06-26
### Breaking Changes
- **Removed:** `GET /v1/orders/{id}/items` — Use `GET /v2/orders/{id}?expand=items` instead
- **Changed:** `total` field renamed to `totalAmount` in all order responses
- **Required:** `X-Request-ID` header now mandatory for all write operations
### New Capabilities
- **Added:** `POST /v2/orders/bulk` — Create up to 100 orders in a single request
- **Added:** `paymentStatus` field to order responses (`pending`, `paid`, `failed`)
- **Added:** Webhook event `order.payment_failed` for failed payment notifications
### Fixes
- **Fixed:** `GET /v2/products` now returns empty array `[]` instead of `null` when no products exist
- **Fixed:** Date fields now consistently return ISO 8601 format with timezone offset
### Deprecations
- **Deprecated:** `customer_id` query parameter — Use `customerId` instead (sunset: 2026-10-01)
- **Deprecated:** `v1` API base path — Migrate to `v2` before 2026-12-01
## 2.4.0 — 2026-05-15
### New Capabilities
- **Added:** `PATCH /v2/orders/{id}` for partial order updates
- **Added:** Rate limit headers (`X-RateLimit-Limit`, `X-RateLimit-Remaining`) to all responses
### Fixes
- **Fixed:** `500 Internal Server Error` on `POST /v2/orders` when `items` array was empty
Operating the Changelog
A changelog that gets written whenever someone remembers is a changelog that stops getting written. Treat it as a release artifact with an owner and a step in the pipeline, not as a document someone maintains.
Who writes the entry. The engineer shipping the change drafts it — they know the consumer impact better than a docs person reconstructing it from commits later. A reviewer (API lead or developer advocate) edits for the consumer-facing questions: does it say what to do, does it name the sunset date, would a consumer who skipped two versions understand it? That review is minutes, not days, if the entry arrives with the PR.
When it publishes. Before or with the deploy, never after. For breaking changes and deprecations the entry goes out ahead of the change — that is the notification, not a report of one. A deprecation posted the same day the header starts flying gives consumers zero warning; the notice period is the point.
Where it lives. One file rendered on the docs site is the baseline. RSS and email reach the consumers who will never reload the page, and the api.changelog_published webhook exists for teams that want to automate their own triage. Internal release notes stay internal — mixing CI work into the consumer page teaches readers to stop reading.
What gets recorded per entry. Version, date, category, and the action the consumer must take — “use GET /v2/orders/{id}?expand=items” — not just what changed. If an entry doesn’t tell the reader whether they need to act, it has failed its one job.
Breaking vs Non-Breaking Decisions
The line that matters to consumers is whether existing client code keeps working unchanged. Everything else is detail.
| Change | Breaking? | Why |
|---|---|---|
| Remove an endpoint or field | Yes | Clients that call/read it break |
| Rename a field or parameter | Yes | Renaming is removing + adding |
| Change a field’s type or format | Yes | total: 42 → total: "42.00" breaks parsers |
| Make an optional field required | Yes | Existing requests start failing validation |
| Change pagination mechanism | Yes | offset→cursor is a contract change |
| Add an endpoint, field, or enum value | Usually no | Unless clients validate responses strictly — say so in the entry |
| Add a required header | Yes | Old requests now reject |
| Tighten a rate limit | Gray zone | Document it prominently even though it’s a policy change, not schema |
| Fix a bug consumers rely on | Gray zone | If the buggy behavior was documented, it’s breaking; if not, fix it and note the inconsistency |
| Deprecate without removing | No | It’s a promise about the future — that’s exactly what the category is for |
When genuinely unsure, treat it as breaking. The cost of over-announcing is a louder changelog; the cost of under-announcing is broken consumers who stop trusting the changelog entirely — and once that trust goes, the page is decoration.
Changelog Automation
Generating changelogs manually is error-prone. Use commit message conventions and tooling to produce changelogs automatically or semi-automatically.
Conventional Commits
Structure commit messages so a tool can parse them:
feat(api): add bulk order creation endpoint
POST /v2/orders/bulk accepts up to 100 orders in one request.
Returns 207 Multi-Status with per-order results.
BREAKING CHANGE: total field renamed to totalAmount in order responses
Node.js Script to Generate Changelog from Git Tags
const { execSync } = "child_process";
function generateChangelog(fromTag, toTag) {
const log = execSync(
`git log ${fromTag}..${toTag} --pretty=format:"%s|%H" --no-merges`
).toString();
const entries = log.split("\n").map((line) => {
const [message, hash] = line.split("|");
return { message: message.trim(), hash };
});
const breaking = entries.filter((e) =>
e.message.includes("BREAKING CHANGE") || e.message.startsWith("feat!")
);
const features = entries.filter((e) =>
e.message.startsWith("feat") && !e.message.includes("BREAKING")
);
const fixes = entries.filter((e) => e.message.startsWith("fix"));
const deprecations = entries.filter((e) =>
e.message.startsWith("deprecate") || e.message.includes("DEPRECATED")
);
let output = `## ${toTag} — ${new Date().toISOString().split("T")[0]}\n\n`;
if (breaking.length) {
output += "### Breaking Changes\n";
breaking.forEach((e) => {
output += `- **Changed:** ${e.message.replace(/BREAKING CHANGE:\s*/, "")}\n`;
});
output += "\n";
}
if (features.length) {
output += "### New Capabilities\n";
features.forEach((e) => {
output += `- **Added:** ${e.message.replace(/^feat\([^)]+\):\s*/, "")}\n`;
});
output += "\n";
}
if (fixes.length) {
output += "### Fixes\n";
fixes.forEach((e) => {
output += `- **Fixed:** ${e.message.replace(/^fix\([^)]+\):\s*/, "")}\n`;
});
output += "\n";
}
if (deprecations.length) {
output += "### Deprecations\n";
deprecations.forEach((e) => {
output += `- **Deprecated:** ${e.message.replace(/^deprecate\([^)]+\):\s*/, "")}\n`;
});
}
return output;
}
console.log(generateChangelog("v2.4.0", "v2.5.0"));
Tools That Generate Changelogs
| Tool | Language | Approach |
|---|---|---|
| semantic-release | JavaScript | Parses conventional commits, auto-generates releases |
| conventional-changelog | JavaScript | CLI to generate changelogs from commit history |
| auto-changelog | JavaScript | Simple CLI, outputs Markdown from git log |
| git-cliff | Rust | Configurable changelog generator from commits |
| changeloguru | Go | Conventional commits to changelog |
Deprecation Headers in Practice
Send HTTP headers in deprecated API responses so consumers can detect deprecation programmatically. Deprecation and Sunset follow RFC 8594 and the deprecation link relation points at the migration guide:
HTTP/1.1 200 OK
Content-Type: application/json
Deprecation: true
Sunset: Sat, 01 Oct 2026 00:00:00 GMT
Link: <https://docs.example.com/api-migration>; rel="deprecation"
Checking Deprecation Headers in Client Code
import requests
response = requests.get("https://api.example.com/v1/orders", headers={
"Authorization": "Bearer token123"
})
if response.headers.get("Deprecation") == "true":
sunset = response.headers.get("Sunset", "unknown date")
link = response.headers.get("Link", "")
print(f"WARNING: This endpoint is deprecated. Sunset: {sunset}")
print(f"Migration guide: {link}")
const response = await fetch("https://api.example.com/v1/orders", {
headers: { Authorization: "Bearer token123" }
});
const deprecation = response.headers.get("Deprecation");
if (deprecation === "true") {
const sunset = response.headers.get("Sunset");
const link = response.headers.get("Link");
console.warn(`Endpoint deprecated. Sunset: ${sunset}`);
console.warn(`Migration guide: ${link}`);
}
Archiving and Retention
The changelog page can’t grow forever — a three-year-old API with monthly releases produces a page nobody scrolls. Set the retention rules once and document them in the versioning policy:
- Live window: keep the last 12 months of entries on the main page. Enterprise consumers on long procurement cycles do look back that far; older entries move to an archive page linked from the header.
- Archive format: same structure, same severity grouping, just older. Don’t downgrade archived entries to a footnote — a consumer upgrading across a year boundary reads the archive as carefully as the live page.
- Sunset memory: keep sunset dates visible in the archive. When someone runs a client two majors behind, “deprecated v1, sunset 2026-12-01” is the information they came for.
- Feed retention: RSS entries can drop after 90 days — the feed is for notification, the archive is for history.
- Never rewrite published entries: correct a mistake with a new dated note (“Correction 2026-10-26: the
totalAmountrename shipped in 2.5.0, not 2.4.0”), not a silent edit. Consumers diff the page; a quiet change reads as gaslighting.
Changelog Review Sign-off
Before a release ships, the entry should survive a two-minute review — run it against these questions:
- Does every breaking change name what to do instead? “Removed
X” without “useY” is a support ticket, not a changelog entry. - Is every deprecation sunsetted? No date means no deadline means no migration urgency — consumers will treat it as optional forever.
- Can a consumer who skipped three versions reconstruct what to do? Entries must compose; if v2.5 says “see the v2.4 notes”, the link better work.
- Did anything consumer-facing ship without an entry? Diff the changelog against the actual deploy — a field that appears in the payload but not in the notes is exactly the silent change this document exists to prevent.
- Is the entry dated and versioned in both places — header and body? A version number in the URL and a different one in the heading is a trust dent that shows up in screenshots.
The reviewer doesn’t need to understand the feature — they need to believe a consumer could act on the entry alone. That’s the bar.
A changelog that passes this review compounds: consumers learn that reading it is enough, so they actually read it, so the next breaking change lands with adoption instead of breakage. The ones that skip it train the same readers to discover changes by diffing responses — at which point the page might as well not exist. The template handles format; this review handles credibility.
For teams just adopting the practice: start with the next release, not a retroactive history. A changelog that begins today and stays disciplined beats a reconstructed one that covers the past and dies in a quarter — the format earns trust through repetition, not completeness.
Once the habit exists, the template becomes infrastructure: consumers stop asking “did anything change?” and start asking “when does 3.1 ship?” — the second question is the one that a healthy API earns from its consumers.
Changelog Format Standards
| Element | Standard | Example |
|---|---|---|
| Version number | Semantic Versioning | 2.5.0 |
| Date format | ISO 8601 (YYYY-MM-DD) | 2026-06-26 |
| Entry verb | Past tense, bolded | **Added:**, **Fixed:** |
| Breaking change marker | First section under each version | ### Breaking Changes |
| Deprecation sunset | ISO 8601 date in parentheses | (sunset: 2026-10-01) |
| Migration link | URL or endpoint reference | Use GET /v2/orders/{id}?expand=items |
Explanation
The changelog uses a version-date-entry hierarchy so consumers can scan for their current version and see everything that changed since. Breaking changes lead — those are the ones that break consumers if missed. Each entry includes a verb (Added, Removed, Fixed, Deprecated) so consumers understand the nature of the change at a glance. The versioning policy section keeps ‘what does this number mean’ questions away.
Variants
| Context | Approach | Notes |
|---|---|---|
| Public API | Full changelog with RSS/email | Consumers self-serve, reduces support tickets |
| Internal API | Slack + short summary | Faster, less ceremony |
| Partner API | Email + migration guide link | Partners need white-glove communication |
| GraphQL API | Schema diff + changelog | Use @deprecated directive in schema alongside changelog |
What Works
- Publish the changelog before deploying the change — not after consumers report issues
- Group entries by severity — breaking changes first, then capabilities, then fixes
- Include migration instructions with every breaking change, not just a description
- Link to documentation for complex new capabilities instead of explaining in the changelog
- Archive old versions but keep them accessible — enterprise clients may be several versions behind
- Use conventional commits so changelogs can be generated or verified automatically
- Send deprecation headers in API responses so client code can detect and warn
Common Mistakes
- Mixing bug fixes and breaking changes without clear categorization
- Writing “misc bug fixes” instead of listing specific fixes consumers care about
- Publishing changelogs days after the deployment — consumers are already broken
- Not versioning the changelog itself — if the changelog changes, consumers lose trust
- Forgetting to document removed fields — consumers discover missing data only in production
- Omitting the sunset date for deprecations — consumers don’t know how much time they’ve got
- Not linking to migration guides from the changelog entry
- Using inconsistent date formats across versions — stick to ISO 8601
- Burying breaking changes at the bottom of a release entry instead of listing them first
Further Reading
- Keep a Changelog — the format standard
- Semantic Versioning — the numbering scheme the changelog tracks
- RFC 8594 — Sunset Header — machine-readable deprecation
Frequently Asked Questions
How far back should the changelog go?
A year of changes is a good baseline online — enterprise consumers on long procurement cycles do look back that far. Anything older goes to an archive page.
Should I document internal-only changes?
No — keep this page consumer-facing. Refactors, CI tweaks, and infra work belong in internal release notes.
What if a bug fix changes behavior that some consumers relied on?
Documented old behavior? Breaking change. Undocumented bug? Fix — with a note: 'Previous behavior was unintentional and inconsistent with documentation.'
Should the changelog be a single file or a page per version?
A single Markdown file covers small APIs. Once releases come frequently, split per major version with an index — the page stays loadable and searchable.
How do I handle pre-release versions?
Tag them with SemVer pre-release labels (3.0.0-beta.1, 3.0.0-rc.1) and say plainly in the entry that things may still change.
Should I include performance improvements in the changelog?
List it only if a consumer notices — a lower latency guarantee, a new caching header. Invisible speed-ups stay in internal notes.
How do I automate changelog publication?
Wire it into CI on tag creation: generate the entry from commits, append it to the changelog, publish to the docs site, and notify subscribed channels.
Related Resources
API Lifecycle Management Template
A copy-paste checklist template for managing API deprecation, versioning transitions, and safe sunset procedures.
DocAPI Deprecation Notice Template
A template for communicating API deprecations, breaking changes, and sunset timelines to consumers.
DocTechnical Specification Template
Use this technical specification template to document requirements, design decisions, architecture, API contracts, and acceptance criteria.
DocSLA Definition Template
A template for defining and documenting Service Level Agreements including uptime targets, response times, error budgets, and escalation procedures.
DocAPI Rate Limiting Policy Template
A template for defining API rate limits per consumer tier, including burst allowances, quota periods, and escalation paths.
GuideComplete Guide to API Versioning Strategies
Compare URI, header, query-param and content-negotiation versioning for REST, plus GraphQL schema evolution, deprecation and migration.