# Release Notes Feed & Webhooks Public RSS/Atom feed and HMAC-signed webhook subscriptions for Chipp platform release notes --- Chipp publishes an official, maintained programmatic surface for platform release notes -- the same content shown on the in-builder [What's New page](/guides/release-notes), with no sign-in required. Read the public RSS/Atom feed, or register a webhook to be notified the moment a new period is published, instead of scraping or polling manually. Both surfaces read from the same release-notes data as the What's New page. There is no separate publish step -- when a new period appears in the builder UI, the feed and any matching webhook subscriptions update immediately. > **Note:** Both surfaces are public and unauthenticated. No API key is required to read the feed or subscribe to webhooks. ## RSS/Atom feed ``` GET /api/v1/release-notes/feed GET /api/v1/release-notes/feed?area=integrations ``` Returns an RSS 2.0 (with Atom namespace) XML document, newest period first. Each `` maps one release-note period: | Field | Source | |-------|--------| | `title` | A human-readable period label, e.g. "Release notes: August 1, 2026" | | `link` | The public What's New page URL | | `guid` | The period's stable id (not a permalink) | | `pubDate` | `periodStart` | | `atom:updated` | `periodEnd` | | `category` | One entry per distinct change kind in the period (`feature`, `improvement`, `fix`, `other`) | | `description` / `content:encoded` | The rendered release-note markdown body | The optional `?area=` query param filters to periods whose items mention that keyword (case-insensitive substring match over each item's title + body, or the raw markdown for periods with no structured items) -- the same area-matching semantics used by webhook subscriptions below. Omit `area` for the full, unfiltered feed. The feed is rate-limited per client IP; a `429` response includes a `Retry-After` header (seconds). ## Webhook subscriptions Register a callback URL to be notified the moment a new release-note period is published, instead of polling the feed. ### Create a subscription ``` POST /api/v1/release-notes/subscriptions Content-Type: application/json { "callbackUrl": "https://your-service.example.com/webhooks/chipp-release-notes", "areas": ["integrations"], "categories": ["feature", "fix"] } ``` | Field | Required | Description | |-------|----------|--------------| | `callbackUrl` | yes | HTTPS endpoint that receives delivery POSTs. Must not point to a private, loopback, or link-local address -- validated at subscribe time and again at every delivery. | | `areas` | no | Case-insensitive keyword filter. A period matches if any subscribed area appears as a substring in an item's title or body (or the raw markdown when a period has no structured items). Omit for no area filtering. | | `categories` | no | One or more of `feature`, `improvement`, `fix`, `other` -- the structured change-category Chipp already derives from each release note. A period matches if it contains at least one item in the requested categories. Omit for no category filtering. | Response (`201 Created`): ```json { "data": { "id": "5b1e6b8a-1c2d-4e3f-9a0b-7c8d9e0f1a2b", "callbackUrl": "https://your-service.example.com/webhooks/chipp-release-notes", "areas": ["integrations"], "categories": ["feature", "fix"], "isActive": true, "maskedSecret": "whsec_••••••1a2b", "secret": "whsec_9f1c...redacted...4d2e", "createdAt": "2026-08-01T12:00:00.000Z", "updatedAt": "2026-08-01T12:00:00.000Z" } } ``` > **Warning:** `secret` is returned in full **exactly once**, in the create response. Store it immediately -- every later response only shows `maskedSecret`. There is no way to retrieve the full secret again; delete and recreate the subscription if it's lost. Creating a subscription immediately backfills the 5 most recent periods matching your filters, delivered through the same signed-payload path as live notifications -- so your system has recent history without waiting for the next release. ### List your subscription There is no Chipp account behind a webhook subscriber, so management is gated by possession of the subscription's own secret rather than a login: ``` GET /api/v1/release-notes/subscriptions Authorization: Bearer whsec_9f1c...redacted...4d2e ``` Returns `{ "data": [...] }` containing the one subscription that secret belongs to (or an empty array if the secret doesn't match anything). ### Delete a subscription ``` DELETE /api/v1/release-notes/subscriptions/{id} Authorization: Bearer whsec_9f1c...redacted...4d2e ``` Returns `204 No Content` on success. A missing or mismatched secret, or an unknown id, returns `404` -- the endpoint deliberately never reveals whether an id exists to a caller who doesn't hold the matching secret. ### Rate limits Both surfaces are rate-limited per client IP: creating subscriptions and listing/deleting a subscription are each capped per client IP. A `429` response includes a `Retry-After` header (seconds). ## Delivery payload Every webhook delivery (live or backfill) POSTs this JSON body: ```json { "event": "release_notes.period_published", "periodStart": "2026-08-01T00:00:00.000Z", "periodEnd": "2026-08-02T00:00:00.000Z", "markdown": "## New Features\n### Slack integration\n...", "categories": ["feature", "fix"], "areas": ["Slack integration"], "timestamp": "2026-08-02T01:00:03.512Z" } ``` `categories` is the distinct set of structured change-categories present in this period. `areas` lists this period's item titles as a hint for client-side filtering -- Chipp's release notes don't carry a separate "product area" tag today, so this is not a fixed taxonomy. ## Verifying signatures Every delivery includes an `X-Chipp-Signature` header: ``` X-Chipp-Signature: sha256=8f434346648f6b96df89dda901c5176b10a6d83961dd3c1ac88b59b2dc327aa ``` The value is an HMAC-SHA256 hex digest of the **raw JSON request body**, computed with your subscription's secret. Verify it before trusting a delivery: ```js import { createHmac, timingSafeEqual } from "node:crypto"; function isValidSignature(rawBody, signatureHeader, secret) { const expected = `sha256=${createHmac("sha256", secret).update(rawBody).digest("hex")}`; const a = Buffer.from(expected); const b = Buffer.from(signatureHeader); return a.length === b.length && timingSafeEqual(a, b); } ``` Use the **raw** request body bytes, not a re-serialized JSON object -- re-serializing can reorder keys or change whitespace and will not match the signature. ## Retry and redelivery behavior A delivery attempt that fails with a network error, a timeout, or an HTTP `408`/`429`/`5xx` response is retried up to twice more with a short backoff (roughly 1s, then 3s), for up to 3 total attempts. A `4xx` response other than `408`/`429` is treated as a permanent rejection and is not retried -- fix your endpoint and Chipp will deliver the next period normally. There is no indefinite retry queue: if all attempts for a period fail, that period is not redelivered later. If you suspect you missed a delivery, recreate your subscription (backfills the last 5 matching periods) or read the [RSS/Atom feed](#rssatom-feed) directly -- it always reflects the full set of currently-published periods, independent of any subscription's delivery history. Deliveries are POSTed with a 10 second timeout and `Content-Type: application/json`. Your endpoint should return a `2xx` status quickly; do heavy processing asynchronously after acknowledging receipt. Deliveries are **at-least-once**: a period can be delivered more than once (for example, if a subscription's backfill overlaps a live delivery of the same newly-published period). Treat delivery as idempotent, keying on `periodStart` + `periodEnd`, so a duplicate is a no-op on your side.