Public API Version 1 No authentication

Changelog JSON API

Fetch published changelog entries for your website, application, or internal tools. Use JSON for structured data, or append .md for one Markdown document. The API is public, read-only, and available for every changelog.

Each published JSON entry includes body_html, body_text, and body_markdown. Add .md to the URL for one Markdown document. You write entries in a rich-text editor built on Lexxy, the Action Text editor from Basecamp. Lexxy stores HTML. Paperstick converts that HTML to Markdown when you read the API.

Quick start

Copy the API URL from Delivery on your changelog, or replace :slug below.

Request
curl --fail --silent --show-error \
  -H "Accept: application/json" \
  "https://paperstick.app/api/v1/changelogs/your-slug?page=1&per_page=25"

Endpoint

Send a GET request. Replace :slug with the slug shown in your dashboard. The endpoint is public and read-only.

GET https://paperstick.app/api/v1/changelogs/:slug
GET https://paperstick.app/api/v1/changelogs/:slug.md
page
Optional integer. Defaults to 1.
per_page
Optional integer. Defaults to 25. Maximum 50.

Markdown document

Request the same endpoint with a .md extension to receive one Markdown document of changelog content. Pagination uses the same page and per_page query parameters. The document does not include API URLs or pagination chrome.

Request
curl --fail --silent --show-error \
  "https://paperstick.app/api/v1/changelogs/your-slug.md?page=1&per_page=25"
200 text/markdown
# Acme

Product updates from the Acme team.

## Faster exports for large reports

*v2.4.0 · improvement · 2026-08-25T09:00:00Z*

## Large reports

Exports now finish faster for reports with more than 10,000 rows. [Read the export guide](https://acme.com/docs/exports).

Example response

The API returns changelog metadata, pagination details, and published entries in newest-first order. Draft entries never appear.

200 application/json
{
  "changelog": {
    "id": 42,
    "name": "Acme",
    "slug": "acme",
    "description": "Product updates from the Acme team.",
    "hosted_page_url": "https://whatsnew.app/acme",
    "api_url": "https://paperstick.app/api/v1/changelogs/acme?page=1&per_page=25",
    "feed_url": "https://whatsnew.app/acme/feed",
    "updated_at": "2026-08-26T08:30:00Z"
  },
  "pagination": {
    "page": 1,
    "per_page": 25,
    "total": 32,
    "total_pages": 2,
    "next_page": 2,
    "previous_page": null
  },
  "entries": [
    {
      "id": 184,
      "title": "Faster exports for large reports",
      "body_html": "<h2>Large reports</h2><p>Exports now finish faster for reports with more than 10,000 rows. <a href=\"https://acme.com/docs/exports\">Read the export guide</a>.</p>",
      "body_text": "Large reports Exports now finish faster for reports with more than 10,000 rows. Read the export guide.",
      "body_markdown": "## Large reports\n\nExports now finish faster for reports with more than 10,000 rows. [Read the export guide](https://acme.com/docs/exports).",
      "kind": "improvement",
      "version": "v2.4.0",
      "released_at": "2026-08-25T09:00:00Z",
      "published_at": "2026-08-25T10:15:00Z",
      "updated_at": "2026-08-25T10:15:00Z"
    }
  ]
}

Field reference

changelog

FieldTypeMeaning
idintegerStable Paperstick changelog ID.
namestringDisplay name.
slugstringURL and API identifier.
descriptionstring or nullOptional changelog description.
hosted_page_urlstring or nullPaperstick page URL. Null when hosting is off.
api_urlstringFirst page of this API endpoint.
feed_urlstringPublic Atom feed URL.
updated_atISO 8601 stringLast changelog update time.

entries[]

FieldTypeMeaning
idintegerStable entry ID.
titlestringEntry title.
body_htmlstringSanitized rich HTML with absolute asset URLs.
body_textstringPlain-text version of the same body.
body_markdownstringMarkdown for the entry body. Headings, links, lists, and emphasis from the rich-text editor are preserved. Asset URLs are absolute.
kindstring or nullnew, fix, improvement, update, or null.
versionstring or nullOptional release version.
released_atISO 8601 string or nullOptional release time.
published_atISO 8601 stringTime the entry became public.
updated_atISO 8601 stringLast entry update time.

Choose HTML, text, or Markdown

body_text

Start here for cards, search indexes, metadata, native apps, and any UI that does not need rich formatting. Set it with textContent.

body_html

Use this when you need links, lists, images, and embeds. Paperstick sanitizes the HTML and makes asset URLs absolute. Apply your own stricter sanitizer or Content Security Policy when your application requires it.

body_markdown

Use this for docs sites, static generators, and tools that prefer Markdown over HTML. Lexxy stores rich text as HTML. Paperstick converts it when you read the API.

Render entries in a browser

This example uses body_text and DOM methods, so API content is never interpreted as HTML.

JavaScript
const endpoint = new URL("https://paperstick.app/api/v1/changelogs/your-slug?page=1&per_page=25");
endpoint.search = new URLSearchParams({ page: "1", per_page: "10" });

const response = await fetch(endpoint, {
  headers: { Accept: "application/json" }
});

if (!response.ok) {
  throw new Error(`Paperstick API returned ${response.status}`);
}

const { changelog, entries } = await response.json();
document.querySelector("h1").textContent = changelog.name;

const list = document.querySelector("#changelog-entries");
for (const entry of entries) {
  const article = document.createElement("article");
  const heading = document.createElement("h2");
  const body = document.createElement("p");

  heading.textContent = entry.title;
  body.textContent = entry.body_text;
  article.append(heading, body);
  list.append(article);
}

// Use entry.body_markdown when your site already renders Markdown.

Pagination

Use next_page until it becomes null. previous_page is null on the first page. A page beyond the available range returns an empty entries array.

JavaScript
async function fetchAllEntries(endpoint) {
  const entries = [];
  let page = 1;

  while (page) {
    const url = new URL(endpoint);
    url.search = new URLSearchParams({ page, per_page: "50" });

    const response = await fetch(url, {
      headers: { Accept: "application/json" }
    });
    if (!response.ok) throw new Error(`Request failed: ${response.status}`);

    const data = await response.json();
    entries.push(...data.entries);
    page = data.pagination.next_page;
  }

  return entries;
}

Caching, CORS, and limits

  • CORS is open for browser GET requests with Access-Control-Allow-Origin: *.
  • Responses include an ETag. Send it back with If-None-Match to receive 304 Not Modified when nothing changed.
  • Each IP can make 120 requests per minute.
  • The endpoint includes X-Robots-Tag: noindex. Index your rendered changelog page, not the raw JSON or Markdown.
  • The JSON API, Markdown document, and Atom feed stay public when you turn off the Paperstick-hosted page.

Errors

Check the HTTP status first, then use error.code for program logic and error.message for logs.

404 application/json
{
  "error": {
    "code": "not_found",
    "message": "Changelog not found"
  }
}