The changelog document: JSON and Atom
Every site you publish answers /changelog.json and /changelog.xml at its own address, and every Changelog space answers the same two files at its own path. They carry what your RSS feed carries and what a script wants besides: each release's version, its categories (added, changed, fixed, deprecated, removed, security), whether it is breaking, and its line items, as structured fields.
What | Address |
|---|---|
The site's changelog |
|
A changelog space |
|
On our address |
|
API |
|
On forumulate.com itself, /changelog.json is the platform's own API changelog and is unchanged; a customer's file is never that one.
Only what everybody may read is in it: published releases (a draft never appears), not members-only ones, and not expired ones. A private changelog answers 404, and a site's root document is fed by a public changelog space only, so nothing private leaks through the site.
The format
JSON Feed 1.1 with the structured fields beside the spec's own. A feed reader that understands JSON Feed reads it as it is; everything else ignores the extra keys.
{
"version": "https://jsonfeed.org/version/1.1",
"title": "Acme changelog",
"description": "What shipped.",
"home_page_url": "https://acme.com/changelog",
"feed_url": "https://acme.com/changelog.json",
"next_url": "https://acme.com/changelog.json?cursor=MTc3MzIy...",
"_changelog": {
"schema": "https://forumulate.com/schemas/changelog-document-v1.json",
"total": 42,
"next_cursor": "MTc3MzIy...",
"updated": "2026-03-04T11:00:00.000Z"
},
"items": [
{
"id": "https://acme.com/changelog/v2-4-0",
"url": "https://acme.com/changelog/v2-4-0",
"title": "Faster exports",
"summary": "Exports are three times faster.",
"content_html": "<h3>Added</h3><ul><li>...</li></ul>",
"content_text": "Added ...",
"date_published": "2026-03-04T10:00:00.000Z",
"date_modified": "2026-03-04T11:00:00.000Z",
"tags": ["added", "breaking", "api"],
"version": "2.4.0",
"type": ["added", "fixed"],
"breaking": true,
"entries": [
{ "type": "added", "text": "CSV export streams instead of buffering." },
{ "type": "fixed", "text": "Export no longer drops the last row." }
]
}
]
}version(document) is the JSON Feed version. The release's own label is the item'sversion, ornull.typeis the list of categories the release has: its declared type, and any tag that names one. Common words map across: new and feature are added, improved and update are changed, bugfix is fixed, retired is removed. A release whose type is none of the six hastype: []and keeps its own tags.breakingis true when the release is marked breaking, or carries a tag in your breaking tags (defaultbreaking,breaking-change). A breaking release also gets thebreakingtag.entriesare the list items of the release body. Under a heading that names a category (### Fixed), an entry is of that category; elsewhere, of the release's single category, or none.date_publishedanddate_modifiedare RFC 3339 UTC.date_modifiedmoves when the release's content is edited, not on a cosmetic change.idandurlare the release's own page (or the changelog page at the release's anchor, see Link style).
/changelog.xml is Atom 1.0 from the same data: each entry has <category term> for the tags, <content type="html">, and, in the https://forumulate.com/ns/changelog namespace, <cl:version>, <cl:breaking> and a <cl:entry type> per line item. One serializer writes both, so they cannot disagree.
Query parameters
Parameter | Meaning |
|---|---|
| Releases published on or after |
| Comma list of categories or tags; a release matching any is kept: |
| Releases per response, up to your configured limit (default 50, at most 500). |
| The previous response's |
|
|
Walk the whole history by following next_url until it is absent.
The response carries an ETag and Last-Modified; send If-None-Match and an unchanged changelog costs a 304. It is served with Access-Control-Allow-Origin: *, so a browser on any origin can fetch it, and cached for a minute at the edge, so a publish shows within that.
Configure it
The same options at both levels. A site's go in the site's config under changelogFeed; a space's go in its settings under changelogFeed. A key a site leaves out falls back to the changelog space that feeds it, then to the default.
Key | Meaning |
|---|---|
|
|
| Site only: which changelog space feeds the site root. Default: the first public changelog space in the site's order. Must be a member of the site. |
| Override the document's title (default: the space's name) and description (default: the space's SEO description). |
| Releases per response, 1 to 500. Default 50. |
| Expose only releases of these categories. Default: all, including releases of no category. |
| Tags that mark a release breaking, besides its own flag. Default |
|
|
Drafts are never exposed; that is not a setting.
In the studio: a changelog space's Feed document section, and the site's Changelog feed group, show the addresses with a copy button, every option above and a preview of the first releases.
Through the API:
# a space
curl -X PATCH https://forumulate.com/api/orgs/$ORG/spaces/$SPACE \
-H "Authorization: Bearer $TOKEN" -H "Forumulate-Version: 2027-03-01" -H "content-type: application/json" \
-d '{"settings":{"changelogFeed":{"limit":20,"categories":["added","fixed","security"]}}}'
# a site: which changelog feeds the root
curl -X PATCH https://forumulate.com/api/orgs/$ORG/sites/$SITE \
-H "Authorization: Bearer $TOKEN" -H "Forumulate-Version: 2027-03-01" -H "content-type: application/json" \
-d '{"config":{"changelogFeed":{"spaceId":"'$SPACE'","linkStyle":"changelog"}}}'A space's key is checked strictly: a misspelled key is a 422 naming it. Add ?dryRun=true to see what a write would do without saving it. null removes the key.
In forumulate.yaml
The same keys, version 2: a space's under spaces[].settings.changelogFeed, a site's under sites[].config.changelogFeed.
version: 2
spaces:
- slug: changelog
type: CHANGELOG
settings:
changelogFeed:
limit: 20
breakingTags: [breaking, migration]
sites:
- slug: main
spaces: [changelog]
config:
changelogFeed:
spaceId: <the changelog space's id>Using it
# everything that broke, since January
curl 'https://acme.com/changelog.json?since=2026-01-01&type=security,fixed' \
| jq '.items[] | select(.breaking) | {version, title, url}'
# a release-notes step in CI
curl -s 'https://acme.com/changelog.json?limit=1' | jq -r '.items[0].content_text'A changelog's index page advertises the document with <link rel="alternate" type="application/json"> in its head, next to the RSS, Atom and JSON Feed links. A site's front page does the same once its changelogFeed names a space or switches the document on.
In the API
Every operation below is also reachable from the other surfaces without a dedicated command. forumulate api <Operation> calls it from the CLI, the JavaScript SDK has a typed method for each one (the last column names it; the Python and Go SDKs are generated from the same spec), and an agent reaches the organization-scoped ones through the MCP agent server — its api_* family tools, or call_api. Where a CLI command or a hand-made MCP tool exists too, the last column names it. Operation names link to the API reference.
Public Changelog Document
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Read the changelog as a JSON document |
| |
| Read the changelog as an Atom document |
| |
| Read the site's changelog as a JSON document |
| |
| Read the site's changelog as an Atom document |
|