Release notes
BranchDeploy already records everything you deploy. Release notes turn that record into customer-facing copy — written and approved by a person, then published to either a hosted page you can link to, or a read-only JSON feed your own app renders.
Nothing reaches customers until someone clicks Publish. Deployment records, branch names, work item IDs, pipeline details and requester names never appear in public output.
How it works
Deployments → candidate list → draft → review → publish → hosted page
└→ public JSON → your app
- You deploy as normal. BranchDeploy records the work item title and ticket type alongside each deployment.
- On the Release notes tab you write a note, pulling in those deployments as source material.
- You preview the exact JSON customers would receive.
- You publish. Only then does anything become public.
Writing your first note
- Open your account page and go to the Release notes tab.
- Click + New.
- Give the note a title — this is the headline customers see.
- Under Candidate deployments, click Add on anything that shipped. Each one copies its work item title in as a starting point and preselects a type from the ticket type: a Bug becomes Fixed, an Epic or Feature becomes New, a Task becomes Maintenance.
- Rewrite each item for customers. The work item title is a prompt, not publishable copy — see writing for customers.
- Click Preview JSON to see exactly what customers would get.
- Click Publish and confirm. Up to that point nothing is public, and you can Save draft as often as you like.
The release date defaults to today and is required, since the feed is ordered by it. Adding a candidate moves it to that deployment’s date, and it stops following them once you set it yourself.
Item types
| Type | Shows as | Use for |
|---|---|---|
feature | New | Something customers could not do before |
improvement | Improved | Something that already worked, now better |
fix | Fixed | A bug customers noticed |
security | Security | Security-relevant changes — keep wording vague |
maintenance | Maintenance | Dependencies, certificates, housekeeping |
other | Update | Anything else |
Option 1 — the hosted page
Every feed gets a page BranchDeploy hosts for you. Nothing to build:
https://branch-deploy.dev/release-notes/rnf_8f2c…
Link it from your app, your footer, or a “What’s new” menu item. It renders your notes as a timeline, works without JavaScript, and is safe to share publicly.
Branding it
Under Release notes → Feed settings you can set:
- Logo URL — must be hosted publicly over HTTPS. Replaces the BranchDeploy mark in the header.
- Homepage URL — adds a “Back to your product” link.
- Brand colour — a 6-digit hex that tints the headings and timeline.
With no logo set the page carries BranchDeploy’s mark, and your feed name is still the heading.
Option 2 — the JSON feed
If you would rather render the notes in your own app, fetch the feed. No authentication, no SDK, no webhook, no database:
const FEED = "https://api.branch-deploy.dev/api/public/release-notes/rnf_8f2c…";
const { notes } = await fetch(FEED).then((r) => r.json());
Response
{
"feed": {
"id": "rnf_8f2c…",
"name": "Release Log",
"updatedAt": "2026-09-28T14:05:09.000Z"
},
"notes": [
{
"id": "rn_123",
"version": "2026.08.27",
"title": "What's new this week",
"summary": "This update improves checkout and account setup.",
"releaseDate": "2026-08-27",
"publishedAt": "2026-08-27T10:30:00Z",
"items": [
{ "type": "feature", "title": "Faster checkout", "body": "Fewer steps to pay." },
{ "type": "fix", "title": "More reliable sign-up" }
]
}
],
"nextPage": null
}
version, summary and an item’s body are omitted when empty rather than returned as null. releaseDate is always present.
Notes are ordered by releaseDate, newest first — not by when you clicked publish. Back-dating a note files it in the right place in the timeline.
Types
type ReleaseNoteItem = {
type: "feature" | "improvement" | "fix" | "security" | "maintenance" | "other";
title: string;
body?: string;
};
type ReleaseNote = {
id: string;
version?: string;
title: string;
summary?: string;
releaseDate?: string;
publishedAt: string;
items: ReleaseNoteItem[];
};
Query parameters
| Parameter | Default | Notes |
|---|---|---|
limit | 10 | Maximum 100 |
cursor | — | The value of nextPage from the previous response, to fetch older notes. Treat it as opaque — it is not a page number. |
nextPage is how you reach older entries. Each response carries the newest notes; if more exist beyond that page, nextPage is a token you pass back as cursor. When it is null there is nothing older — most feeds will never return anything else, and a “What’s new” panel showing the latest few can ignore it entirely.
A “What’s new” badge wants ?limit=1. A full changelog page wants ?limit=100 — that covers a year of weekly releases in one request. Beyond that, follow nextPage:
let url = `${FEED}?limit=100`;
const all = [];
while (url) {
const { notes, nextPage } = await fetch(url).then((r) => r.json());
all.push(...notes);
url = nextPage ? `${FEED}?limit=100&cursor=${encodeURIComponent(nextPage)}` : null;
}
A single note
GET /api/public/release-notes/{publicFeedId}/{noteId}
Returns { feed, note }. Useful for deep-linking one release.
Knowing when it changed
feed.updatedAt moves whenever anything in the response changes — a note published, a live note edited, one archived or deleted, or the feed renamed. Saving a draft does not move it, because nothing public changed.
Store it alongside whatever you cache and compare on your next fetch:
const { feed, notes } = await fetch(FEED).then((r) => r.json());
if (feed.updatedAt !== lastSeen) {
render(notes);
lastSeen = feed.updatedAt;
}
It only ever moves forward, so it is also safe for “New updates” badges.
Caching
Responses carry Cache-Control: public, max-age=60, stale-while-revalidate=300 and an ETag. Send If-None-Match and you will get a 304 when nothing has changed — cheaper than comparing updatedAt, and the right choice if you only want to avoid re-downloading.
Handling failure
Treat the feed as decorative. If it fails, hide the panel — never block your app on it:
async function loadReleaseNotes() {
try {
const res = await fetch(FEED, { headers: { Accept: "application/json" } });
if (!res.ok) return [];
return (await res.json()).notes ?? [];
} catch {
return [];
}
}
Your feed URL never changes
The ID in your URL is random, generated once, and never regenerated. Renaming the feed does not change it. Publishing, archiving and editing do not change it. It is safe to hardcode in a client app.
Turning public access off in Feed settings makes both URLs return 404 immediately, without unpublishing anything — useful if something goes out wrong. Turning it back on restores them.
Editing, archiving and deleting
A note you have not published yet is a draft. Drafts are private — save as often as you like, nothing is public until you click Publish.
Once a note is live, Save changes updates it for customers straight away. There is no second publish step. Its original publication date is kept, so an edit does not push it back to the top of the feed.
Two ways to take a note down:
| What happens | Reversible | |
|---|---|---|
| Archive | Withdrawn from the public feed immediately, including from anyone who linked to it. The wording is kept. | Yes — publish it again |
| Delete | The note and its items are removed permanently. | No |
Archive is the safe option. Delete is for notes published by mistake, or drafts you no longer want.
Writing for customers
The deployment record is written by developers for developers. Customers need something else:
- Write the benefit, not the change. “Checkout takes one screen instead of three”, not “Refactored checkout controller”.
- No branch names, work item IDs, or pipeline names.
- Avoid “fixed bug #1234” — say what works now.
- Keep titles short. The body is one or two sentences.
- Keep security notes vague unless you have a reason not to.
- Never describe something the deployments do not actually support.
With an AI assistant (MCP)
If you use AI deployment, your assistant can draft release notes from what shipped:
Draft release notes for this week’s UAT deployments. Keep it short and avoid anything internal.
The assistant reads your deployments, writes customer-facing copy, and saves a draft. It can then refine it on request. Publishing always requires you to confirm explicitly, against a preview of the exact content — if the note changes after that preview, the confirmation is void and it has to ask again.
Assistants can list existing notes, create and refine drafts, preview, publish, and archive. They cannot edit or delete a note that is already published — those go through the account page, so a person stays in the loop on anything customers are already reading.
Letting the assistant read your tickets
By default an assistant sees only what a deployment recorded: the work item ID, its title, type and state. Titles are often terse (“Fix #4102 regression”), which makes for a vague release note.
Turning on AI drafting under Release notes → Feed settings lets the assistant also read the description on the Azure DevOps ticket behind each deployment, so it can write from what the work actually was.
It is off until you turn it on, and worth a moment’s thought before you do:
- Ticket descriptions are internal. They routinely contain customer names, incident detail, internal system names, and commercial context.
- Switching it on means that text can be sent to whichever assistant you have connected, under that provider’s own data handling.
- Only tickets attached to deployments BranchDeploy already recorded can be read — an assistant cannot ask for an arbitrary work item, or for anything in a project it has not deployed.
- Descriptions are converted to plain text and capped; attachments, images and links are dropped.
- It changes nothing about publishing. Drafts still need your explicit confirmation, and nothing reaches customers until you publish.
You can turn it off again at any time, and the next request will be refused.
Writing notes in the account page is unaffected either way — the ticket panel under each item has always shown you the description, because that is you reading your own ticket in your own browser.
Who can do what
| Action | Owner | Admin | Editor | Viewer |
|---|---|---|---|---|
| View notes and the feed URL | Yes | Yes | Yes | Yes |
| Create and edit drafts | Yes | Yes | Yes | — |
| Publish and archive | Yes | Yes | Yes | — |
| Delete permanently | Yes | Yes | Yes | — |
| Feed settings and branding | Yes | Yes | — | — |
| Turn AI drafting on or off | Yes | Yes | — | — |
Feeds
A feed is created automatically for each project the first time it syncs from the extension — there is nothing to set up. It is named Release Log until you rename it under Feed settings; that name is the heading on the hosted page and the feed.name in the JSON.
One feed per project. If you have not synced a project yet, open Project Settings → BranchDeploy in Azure DevOps and save.
Troubleshooting
The public URL returns 404. Public access is off for the feed, or nothing has been published yet. Check Feed settings.
A candidate has no title. The deployment was recorded by an older version of the Azure DevOps extension, which did not send work item titles. New deployments will have them. Update the extension from the Marketplace.
My edits are not showing publicly. Saving a live note updates it immediately, but the feed is cached for 60 seconds — wait a moment and reload.
An item came out as the wrong type. The type is a suggestion from the Azure DevOps ticket type. Change the dropdown — it is only a starting point.