Naar de inhoud

Documentatie voor ontwikkelaars

Laatst bijgewerkt: 26 september 2026.

Alles wat Changeloop voor je publiceert is gewone JSON over HTTPS. Er is geen SDK te installeren, geen API-sleutel om te rouleren en geen inlogstap: de twee feeds hieronder zijn anonieme publieke leesbewerkingen op basis van je feed-id. Vervang YOUR_PUBLIC_ID door dat van jou in elk voorbeeld op deze pagina.

Eén ding om te weten voordat je begint: je publieke feed-id staat in de app zelf. Log in, open Instellingen, en het staat daar meteen in de sectie Publieke feed, waar je standaard op terechtkomt, samen met kant-en-klare links naar changelog.json en roadmap.json, een link naar je gehoste feedpagina en het widget-fragment hieronder, elk met een eigen kopieerknop.

Aan de slag

In vijf stappen ga je van aanmelden naar een changelog op je eigen site. De pagina "Get started" in de app loodst je erdoorheen en vinkt elke stap af zodra die klaar is.

  1. Koppel een bron: een GitHub-repository, een GitLab-project of een Bitbucket-repository.
  2. Kies de taal waarin je items worden geschreven.
  3. Maak eventueel tags aan, zodat lezers op productonderdeel kunnen filteren.
  4. Publiceer je eerste item. Samengevoegde wijzigingen komen als concept binnen in de controle-inbox: keur er een goed, of zet automatisch publiceren aan voor die repository.
  5. Zet het op je site: link naar je gehoste pagina, plak de widget of toon de JSON-feed op je eigen pagina.

Je changelog in ongeveer tien regels React

Plak dit in een component en je hebt een werkende changelog. Er is niets anders toe te voegen.

import { useEffect, useState } from 'react';

const FEED = 'https://api.changeloop.dev/v1/public/YOUR_PUBLIC_ID/changelog.json';

export function Changelog() {
  const [entries, setEntries] = useState([]);
  useEffect(() => {
    fetch(FEED).then((r) => r.json()).then((feed) => setEntries(feed.data));
  }, []);
  return <ul>{entries.map((e) => <li key={e.id}><b>{e.title}</b><p>{e.mdContent}</p></li>)}</ul>;
}

mdContent is de markdown die we hebben opgesteld, als tekst. Wil je liever opgemaakte uitvoer renderen, gebruik dan htmlContent: dat wordt serverzijdig gebouwd door onze eigen sanitizer, vanuit een vaste lijst met toegestane tags en attributen, en het is de enige waarde in al deze antwoorden die bedoeld is om als opmaak te worden ingevoegd. Al het overige is tekst, en items die zijn opgesteld vanuit een publieke repository kunnen worden beïnvloed door iedereen die daar een pull request kan openen, behandel ze dus dienovereenkomstig.

De changelog-feed

GET/v1/public/YOUR_PUBLIC_ID/changelog.json

Je gepubliceerde items, nieuwste eerst, waarbij de recentste id de doorslag geeft bij identieke tijdstempels.

Queryparameters

  • repos accepteert een lijst met volledige repositorynamen, gescheiden door komma's, bijvoorbeeld acme/web,acme/api. Er komen alleen items terug uit die repositories. Laat het weg en je krijgt ze allemaal.
  • limit is hoeveel items je per pagina wilt. De standaardwaarde is 20, alles boven 50 wordt beperkt tot 50, en alles wat we niet als positief getal kunnen lezen valt terug op 20 in plaats van te mislukken.
  • cursor is ondoorzichtig. Neem de waarde nextCursor uit het vorige antwoord en geef die ongewijzigd terug. Een cursor die we niet kunnen decoderen, wordt behandeld als geen cursor, dus krijg je opnieuw de eerste pagina in plaats van een fout.

Antwoord

{
  "data": [
    {
      "id": "66b0c1f2e4a9d1c3b5a70011",
      "title": "Saved views on the inbox",
      "mdContent": "You can now pin a filter and come back to it.",
      "htmlContent": "<p>You can now pin a filter and come back to it.</p>",
      "repoFullName": "acme/web",
      "category": "feature",
      "tags": ["Inbox"],
      "learnMoreUrl": "https://acme.example/docs/saved-views",
      "publishedAt": "2026-08-06T09:12:44.000Z"
    }
  ],
  "nextCursor": null,
  "tagColors": { "Inbox": "#4f46e5" }
}

Elk item heeft dezelfde negen sleutels: id, title, mdContent, htmlContent, repoFullName, category, tags, learnMoreUrl en publishedAt. category is feature, fix of internal, en is null wanneer degene die het opstelde er geen instelde, publishedAt is een ISO 8601-tekenreeks, en htmlContent is een lege tekenreeks bij een item dat nooit door de opsteller is gegaan. tags is een array van je eigen productgebiednamen en is leeg als er geen zijn toegewezen, learnMoreUrl is null tenzij iemand die tijdens controle heeft toegevoegd, en de kleur voor elke tag komt uit de tagColors-map van het antwoord, niet uit het item, dus een tag die je inmiddels uit je vocabulaire hebt verwijderd, wordt gewoon zonder kleur gerenderd. nextCursor is null wanneer je het einde hebt bereikt.

Een onbekende feed-id reageert met 404 en {"error":"not_found"}, net als een verkeerd gevormde. De twee zijn bewust niet van elkaar te onderscheiden, zodat dit endpoint niet gebruikt kan worden om te achterhalen welke id's bestaan.

De roadmap-feed

GET/v1/public/YOUR_PUBLIC_ID/roadmap.json

Dezelfde drie kolommen die je team met de hand bijhoudt.

{
  "columns": [
    { "column": "planned", "items": [], "hasMore": false },
    {
      "column": "building",
      "items": [
        {
          "id": "66b0c1f2e4a9d1c3b5a70042",
          "column": "building",
          "publicTitle": "Slack notifications",
          "publicDescription": "Post each published entry to a channel you pick.",
          "publishedAt": "2026-08-05T16:20:01.000Z"
        }
      ],
      "hasMore": false
    },
    { "column": "shipped", "items": [], "hasMore": false }
  ]
}

columns is een array, geen object geïndexeerd op kolomnaam, en de volgorde ervan is onderdeel van het contract: planned, dan building, dan shipped. Alle drie zijn altijd aanwezig, ook de lege, zodat je nooit 'deze kolom bestaat niet' hoeft te onderscheiden van 'heeft nog niets'. Render ze in de ontvangen volgorde en je komt overeen met elk ander oppervlak dat we bouwen.

Een item heeft precies vijf sleutels: id, column, publicTitle, publicDescription en publishedAt. publicDescription is altijd een tekenreeks en kan leeg zijn, nooit null. Niets over het issue waar een item vandaan komt, wordt hier blootgegeven, niet de repository en niet het issuenummer, en dat is bewust, geen omissie die we later invullen.

Dit endpoint accepteert helemaal geen queryparameters. Er is geen cursor, geen limit en geen repositoryfilter, want een roadmap is een klein bord dat een persoon samenstelt, geen log dat oneindig groeit. Elke kolom geeft tot 50 items terug en zet hasMore als er meer waren. hasMore is informatief: er is geen cursor om te volgen, bouw er dus geen paginering omheen.

publicTitle en publicDescription zijn gewone tekst, opgesteld vanuit issue-titels en -inhoud, die op een publieke repository door iedereen beïnvloed kunnen worden die daar een issue opent. Ze hebben geen garantie voor HTML-opschoning en zijn niet de htmlContent-uitzondering. Render ze als tekst.

De insluitbare widget

Als je liever niets bouwt, voeg dan deze twee regels toe. De widget is een custom element dat rendert in een shadow root, waardoor het je stijlen niet overneemt en er ook niet in lekt.

<script src="https://api.changeloop.dev/widget.js" defer></script>
<changelogapp-widget
  data-public-id="YOUR_PUBLIC_ID"
  data-api="https://api.changeloop.dev"></changelogapp-widget>

Beide attributen zijn verplicht. data-public-id is je feed-id, data-api is de oorsprong waarvandaan de widget laadt. Ontbreekt een van beide, dan schrijft het element een fout naar de console en rendert het helemaal niets, wat het eerste is om te controleren als je een lege ruimte ziet waar het zou moeten staan.

Voeg data-theme="dark" toe aan het element voor een donkere weergave; je pagina kan dit tijdens runtime omschakelen. Voor verdere styling stelt de widget CSS custom properties (--changelogapp-text, --changelogapp-bg, --changelogapp-accent en meer) en ::part()-namen beschikbaar, die je in je eigen stylesheet instelt. De app toont beide thema's live onder Instellingen, Publieke feed.

Voeg data-repos toe om slechts een deel van je repositories te tonen, bijvoorbeeld de changelog van één product op de site van dat product wanneer meerdere producten één account delen. De waarde is een door komma's gescheiden lijst van volledige namen, owner/repo; een naam zonder eigenaar komt nergens mee overeen en toont een lege feed zonder foutmelding. Maximaal tien repositories worden meegenomen. Een zo afgebakende widget toont alleen Updates en Feedback, omdat de roadmap geen weergave per repository heeft, en feedback wordt nog steeds ingediend waar het feedbackdoel van je team naar wijst. Onder Instellingen, Openbare feed staat een kiezer die het attribuut voor je schrijft.

Het rendert drie tabbladen in deze volgorde: Nieuws, Roadmap en Feedback. De eerste twee lezen de feeds hierboven. De derde stuurt naar het endpoint hieronder en bewaart elke verzend-id in localStorage, zodat een bezoeker kan terugkomen en zien wat er is gebeurd met wat hij heeft verzonden.

Het script wordt versied aangeboden. /widget.js levert altijd de nieuwste build en wordt een uur gecachet, zodat een release je bezoekers bereikt zonder dat je iets hoeft aan te raken. /widget-vN.js zet één build vast: zodra een versienummer is aangeboden, veranderen de bytes ervan nooit meer, en het wordt een jaar gecachet. Zet het vast als je wijzigingen liever bewust doorvoert.

Laad precies één widgetscript per pagina

De twee URL's zijn alternatieven, geen lagen. Beide registreren dezelfde naam voor het custom element, en een browser laat een naam maar één keer per document registreren: welk script het eerst wordt uitgevoerd, wint, voor de hele levensduur van de pagina, en het tweede blijft inert. Een pagina met zowel /widget.js als /widget-v5.js rendert dus wat de browser toevallig als eerste heeft uitgevoerd, en dat heb je niet in de hand; /widget-v5.js naast een bestaande /widget.js zetten om de versie vast te leggen doet helemaal niets. Meestal wint de oudere build, omdat die al in de cache zit.

Als dat gebeurt, schrijft de widget een waarschuwing naar de console met beide buildnamen, zodat je niet hoeft te raden. Meer dan waarschuwen kan het niet: tegen de tijd dat de tweede kopie draait, heeft de eerste de naam al in bezit. De oplossing is altijd de scripttag te vervangen in plaats van er nog een toe te voegen, en hetzelfde geldt als een tagmanager of een partial er een voor je injecteert. Om van de doorlopende build naar een vastgezette over te stappen, wijzig je de src.

De gehoste feedpagina

https://feed.changeloop.dev/feed/YOUR_PUBLIC_ID

Op datzelfde adres hosten we ook een gewone pagina: je changelog en je roadmap-bord, gerenderd vanuit dezelfde twee feeds hierboven. Er is geen inloggen of instellen aan jouw kant voor nodig. Het is ook waar we mensen naar terugsturen zodra een lus zich sluit: de Shipped-reactie die we op een GitHub-issue plaatsen, linkt hierheen, net als shippedEntry.link uit de statuscontrole hierboven, beide belanden ze op het uitgeleverde item met zijn eigen #entry-ID-anker, dat het item nog steeds vindt zelfs als het inmiddels naar een latere pagina is verplaatst.

Behandel het als een terugvaloptie, niet als de integratie. De changelog-feed en de widget blijven de manier om dit in je eigen site te zetten zodat het aanvoelt als jouw product en niet als het onze; deze pagina is bedoeld voor de periode voordat je dat hebt gedaan, en voor loop-close-links, die hierheen wijzen ongeacht wat je verder hebt gebouwd.

Je eigen domein

Je kunt de gehoste pagina vanaf je eigen adres serveren, zonder DNS- of certificaatwijzigingen. Plak onder Instellingen, Eigen domein het publieke adres dat je lezers zien (bijvoorbeeld https://example.com/changelog) en wijs dat pad op je site naar het proxydoel dat daar staat: één regel dekt de pagina, de assets, de data en de feeds. Controleer mijn domein haalt je adres van onze kant op en vertelt je of de proxy klopt en, zo niet, wat je moet aanpassen.

De MCP-server

POSThttps://api.changeloop.dev/mcp

Werk je in Claude Code, ChatGPT of een andere agent die het Model Context Protocol spreekt, dan kun je die rechtstreeks aan je changelog koppelen. De agent kan dan zien wat wacht op controle, de tekst bewerken en publiceren, zonder dat je de editor verlaat. Het is dezelfde controlepoort als in de webapp: niets wordt openbaar tot iets het goedkeurt.

Claude Code koppelen

Maak eerst een API-sleutel aan (Instellingen, API-sleutels), en voeg dan de server toe met je sleutel in de header:

claude mcp add --transport http changeloop \
  https://api.changeloop.dev/mcp \
  --header "Authorization: Bearer clapi_YOUR_KEY"

Voor een client die in plaats daarvan een JSON-configuratie leest, ziet hetzelfde er zo uit:

{
  "mcpServers": {
    "changeloop": {
      "type": "http",
      "url": "https://api.changeloop.dev/mcp",
      "headers": { "Authorization": "Bearer clapi_YOUR_KEY" }
    }
  }
}

Er is nog geen OAuth-flow. De authenticatie is de API-sleutel in de header, wat precies is wat de twee commando's hierboven doen. Die sleutel intrekken in Instellingen koppelt de agent los bij zijn volgende verzoek.

Wat de agent kan doen

Zeven tools, en de lijst is bewust kort. Al het overige dat dit product kan, is bereikbaar via de REST-API met dezelfde sleutel; elke tool die aan een agent wordt blootgesteld, is nog iets waartoe hij overgehaald kan worden.

  • list_pending_entries, list_published_entries, get_entry - lezen je items. Wachtende items zijn niet openbaar.
  • update_entry - wijzigt de titel of de markdown-body van een item. De HTML die de feed aanbiedt, wordt door onze sanitizer opnieuw gegenereerd vanuit je markdown; een agent kan geen HTML leveren.
  • approve_entry - publiceert. Dit is openbaar en direct, en informeert gekoppelde feedback op GitHub. Alleen een wachtend item kan worden goedgekeurd.
  • discard_entry - houdt een item buiten de changelog. Omkeerbaar vanuit de webapp.
  • get_changelog_info - je feed-id en de adressen waarvandaan je changelog wordt aangeboden.

Wat het niet kan

Elke tool is beperkt tot het team waartoe de sleutel behoort, en geen enkele accepteert een team als argument, dus is er niets waarmee naar een ander team gewezen kan worden, ook al zou iemand het proberen. De server accepteert geen browsersessie, alleen een sleutel: een verzoek moet de credential bewust meesturen. En een sleutel kan geen sleutels beheren en je gegevensexport niet downloaden, dus een op deze manier gekoppelde agent kan zichzelf geen tweede credential aanmaken en je gegevens niet in één aanroep onttrekken.

API-sleutels

Al het bovenstaande is anoniem en vereist geen credential. De geauthenticeerde API, je instellingen en je controle-inbox, is een ander oppervlak, en die accepteert een aangemelde browsersessie of een API-sleutel. Sleutels zijn voor scripts en agents: alles wat je changelog moet bereiken zonder een mens achter een toetsenbord.

Authorization: Bearer clapi_YOUR_KEY

Maak er een aan in de app, onder Instellingen, op het tabblad API-sleutels. De sleutel wordt één keer getoond, op het moment dat je hem aanmaakt, en daarna nooit meer: we bewaren alleen een hash ervan, dus er is geen scherm dat hem je een tweede keer kan tonen. Verlies je hem, trek hem dan in en maak een nieuwe aan.

Wat een sleutel wel en niet kan

Een sleutel heeft dezelfde toegang als inloggen, beperkt tot het ene team waarin hij is aangemaakt, met twee bewuste uitzonderingen. Hij kan geen API-sleutels beheren, en hij kan je gegevensexport niet downloaden. Beide vereisen een echte login, zodat een gelekte sleutel geen vervanging voor zichzelf kan aanmaken, de sleutels waarmee je hem zou blokkeren niet kan intrekken, en de gegevens van je team niet in één verzoek kan onttrekken.

Intrekken

Een intrekking gaat in bij het volgende verzoek. Een ingetrokken sleutel antwoordt met 401, precies als een onbekende, en blijft met 401 antwoorden zelfs vanuit een browser die nog een geldige sessie heeft, want een verzoek met een Authorization-header wordt nooit stilzwijgend opnieuw geprobeerd als cookieverzoek. De ingetrokken sleutel blijft vermeld met de datum van intrekking en die van laatste gebruik, wat precies is wat je nodig hebt om te achterhalen wat een gelekte sleutel heeft bereikt.

Abonnementen en limieten

Het gratis abonnement schrijft 20 gemergde wijzigingen per maand uit en beperkt het dagelijkse aantal bekeken merges, getriageerde feedbackinzendingen, opgestelde roadmapkaarten en alternatieve versies tot elk 50; het teamabonnement heeft geen harde limieten. Instellingen, Abonnement en gebruik toont elk budget zoals het product het zelf telt, met het moment waarop het wordt gereset, voordat iets wordt geweigerd. Werk dat boven een limiet binnenkomt wordt vastgehouden, niet weggegooid: een item boven het quotum wacht in de inbox, en een geweigerd roadmapconcept kun je opnieuw proberen zodra het venster doorschuift.

GitLab en Bitbucket

Een GitLab-project of een Bitbucket-repository kan je changelog voeden net als een GitHub-repository: koppel het onder Instellingen, dan GitLab, of Instellingen, dan Bitbucket, voeg de webhook toe die we je geven (of laat Connect with Bitbucket dat op bitbucket.org doen als de Bitbucket-pagina die knop aanbiedt), en elke wijziging die wordt samengevoegd in de branch die je opgeeft, wordt een conceptitem in je controle-inbox, op dezelfde manier geschreven en onderworpen aan dezelfde menselijke controle. Items komen uit samengevoegde pull requests of merge requests, of, op GitHub en Bitbucket, uit pushes als je onder Instellingen, dan What creates drafts, voor pushmodus kiest. GitLab-projecten maken alleen concepten uit merge requests.

Een project koppelen

GitLab-projecten koppel je onder Instellingen, dan GitLab, Bitbucket-repository's onder Instellingen, dan Bitbucket. Voer het pad in (op GitLab de groep en het project, zoals acme/web, op Bitbucket de workspace en de repository, zoals acme/app) en we geven je een webhook-adres en een secret terug. Plak beide in de webhookinstellingen aan de andere kant: vink op GitLab Merge request events aan, vink op Bitbucket de triggers Merged pull request en Push repository aan. Zelf gehoste instanties werken, via https. Het secret wordt één keer getoond, op dat moment. Verlies je het, verwijder dan het project en koppel het opnieuw. Op bitbucket.org, als de Bitbucket-pagina een knop Connect with Bitbucket toont, kun je het plakken overslaan: druk erop, geef één keer toegang, en wij lezen de hoofdbranch van de repository en voegen de webhook voor je toe. Je hebt beheerdersrechten op de repository nodig. Bij een zelf gehoste Bitbucket-server, of als je liever plakt, kies je Set it up by hand en krijg je het adres en het secret zoals hierboven. Verwijder je een Bitbucket-repository en koppel je die opnieuw, verwijder dan ook de oude webhook op Bitbucket, onder Repository settings, dan Webhooks. Zodra een project is gekoppeld, kun je in de rij ervan de branch wijzigen en automatisch publiceren aanzetten, en als een levering is genegeerd, staat in de rij waarom.

Waarom Bitbucket om een branch vraagt en GitLab niet

GitLab vertelt ons welke branch je project als standaard behandelt, dus je kunt het veld leeg laten en precies dat bedoelen. Bitbucket stuurt helemaal geen standaardbranch, dus als we je het veld leeg zouden laten laten, zouden we niets hebben om mee te vergelijken, en zou je webhook er perfect geïnstalleerd blijven uitzien zonder ooit één item te produceren. Dan stellen we liever een vraag dan dat te laten gebeuren. Met Connect with Bitbucket vragen we Bitbucket om de hoofdbranch zodra je toegang geeft, dus je hoeft die niet in te typen.

Wat ze nog niet dekken

Changelog-items, en verder niets. Dat de feedbackwidget voor jou een issue opent, dat de reactie op dat issue wordt geplaatst zodra de fix uitkomt, dat de publieke roadmap wordt gevoed door issue-labels, en de bronvoorvertoning in de controle-inbox, dat is vandaag allemaal exclusief voor GitHub.

We zeggen liever de reden dan die te verbloemen. Elk van die dingen vereist een toegangstoken met schrijfrechten op je project, dat bij ons zou blijven. Changelog-items hebben er geen nodig, omdat alles waaruit ze worden geschreven in de webhook zelf binnenkomt, dus GitLab of Bitbucket via de webhook koppelen geeft ons geen credential en geen leestoegang tot je code. Connect with Bitbucket is de enige uitzondering. Bitbucket leent ons voor één enkel verzoek een token dat de repository en de pull requests ervan kan lezen en de webhooks ervan kan beheren, dat we alleen gebruiken om de hoofdbranch te lezen en de webhook toe te voegen, en daarna weggooien. Er wordt niets bewaard. We leveren liever het deel dat je niets kost dan om een token te vragen om een functielijst af te ronden.

Andere versies van een item

Een wijziging moet vaak meer dan eens worden uitgelegd: aan klanten in de changelog, aan wie er vragen over beantwoordt, en in een kanaal waar niemand vier alinea's leest. Vanuit de controle-inbox kun je twee extra versies van een item opstellen voordat je het goedkeurt.

Een aankondigingsversie is één of twee regels, en dat is wat naar Slack wordt gepost wanneer je het item goedkeurt, in plaats van de volledige tekst. Een supportnotitie is een interne briefing: wat er is veranderd, wat klanten zullen merken, en een zin die iemand van support bijna letterlijk zou kunnen zeggen. Beide zijn concepten die je kunt herschrijven voordat ze worden gebruikt, en beide kunnen worden verwijderd.

Geen van beide wordt gepubliceerd

Deze versies verschijnen nooit op je changelog-pagina, in geen enkele feed, in de widget of in de API die ze aanbiedt. Vooral de supportnotitie is geschreven voor mensen binnen je bedrijf en kan directer zijn dan het item zelf. Hij bestaat alleen in je controle-inbox en, als je ze gebruikt, in je eigen kopie.

Waaruit ze worden geschreven

Altijd vanuit het item, nooit vanuit de pull request. Dat is bewust: het item is al door de regel gegaan die beveiligingsfixes vaag houdt, en door jouw eigen controle. Een versie die daaruit is herschreven kan geen detail terugbrengen dat je hebt verwijderd, omdat dat detail niet in wat het model kreeg zit.

Aankondigen in Slack

Keur een item goed en het kan op hetzelfde moment dat het openbaar wordt naar een Slack-kanaal worden gepost. Koppel dat onder Instellingen, op het tabblad Slack: maak een inkomende webhook aan in je eigen workspace, kies het kanaal en plak de URL. Verder dan die webhook wordt er niets geïnstalleerd aan jouw kant, en we vragen geen toegang tot je workspace.

Het bericht draagt de itemtitel, de tekst zoals jij die hebt goedgekeurd, categorie en tags, en een link terug naar het item op je changelog. Markdown wordt vertaald naar wat Slack echt rendert, dus een item komt niet aan met zichtbare eigen sterretjes.

De webhook-URL is een credential

Wie die URL heeft, kan in het kanaal posten, dus behandelen we hem als een wachtwoord: hij wordt opgeslagen, en daarna toont geen scherm en geen API-antwoord hem ooit weer, ook je eigen gegevensexport niet. Wat je daarna ziet is een masker, genoeg om twee webhooks van elkaar te onderscheiden en nutteloos voor ieder ander. We accepteren alleen een hooks.slack.com-adres, dus een verkeerd getypte of vervangen URL wordt geweigerd in plaats van opgehaald.

Wanneer het stopt met werken

Verwijder je de app in Slack of archiveer je het kanaal, dan stopt de webhook permanent met werken. We merken dat bij het eerste geweigerde bericht, schakelen aankondigingen uit en vermelden dat op het tabblad Slack met de reden en de datum. Dat we niet stilzwijgend blijven proberen, is bewust: een changelog die niemand heeft aangekondigd, ziet er precies zo uit als een die niemand heeft gelezen, en dat verschil is het waard om te worden gemeld.

Pauzeren

Pauzeren stopt de aankondigingen en behoudt de webhook, dus hervatten is één klik in plaats van nog een tocht door Slack. Loskoppelen verwijdert de URL volledig. In beide gevallen wordt publiceren zelf niet beïnvloed: Slack is een kanaal waar je changelog naartoe post, nooit een poort waarop het wacht. Is Slack onbereikbaar op het moment dat je iets goedkeurt, dan wordt het item toch gepubliceerd en wordt de aankondiging vanzelf opnieuw geprobeerd.

RSS en JSON Feed

GEThttps://api.changeloop.dev/v1/public/YOUR_PUBLIC_ID/rss.xmlGEThttps://api.changeloop.dev/v1/public/YOUR_PUBLIC_ID/feed.json

Dezelfde gepubliceerde items als abonneerbare feed, in de twee formaten die feedlezers begrijpen: RSS 2.0 en JSON Feed 1.1. Beide accepteren dezelfde filters repos, category en tag als de changelog-feed en dragen dezelfde Cache-Control en ETag. Geen van beide pagineert: een lezer vraagt de kop van de feed op, dus deze geven alleen de meest recente items terug, zonder cursor.

De tekst van het item is de opgeschoonde HTML, verpakt in CDATA voor RSS en als content_html voor JSON Feed. JSON Feed draagt daarnaast de kleuren van je tags onder een namespaced _changelogapp-extensie; RSS niet, omdat geen lezer die zou tekenen.

De gehoste pagina kondigt beide aan als rel="alternate"-links, zodat een browser of lezer die daar landt zich kan abonneren zonder dat de paden hoeven te worden meegedeeld.

Eén item op zich

GEThttps://api.changeloop.dev/v1/public/YOUR_PUBLIC_ID/entries/ENTRY_ID

Geeft één gepubliceerd item terug, hetzelfde object dat de changelog-feed in zijn data-array draagt. Daar verwijzen de permalinks in de feeds naartoe, en het is nuttig als je een id hebt en niet door de feed wilt bladeren om die te vinden. Een onbekende id, of een van een niet-gepubliceerd item, geeft 404 met dezelfde body als elke andere onbekende id.

De markdown-feed

GEThttps://api.changeloop.dev/v1/public/YOUR_PUBLIC_ID/changelog.md

Dezelfde gepubliceerde items als gewone markdown, aangeboden als text/markdown. Het bestaat voor lezers die geen browsers zijn: een LLM of agent die antwoordt op wat er onlangs in dit product is veranderd, krijgt de tekst zonder RSS te parsen of JSON te doorlopen. Het accepteert dezelfde filters repos, category en tag als de changelog-feed, draagt dezelfde Cache-Control en ETag, en reageert net als de andere twee met 304 op een voorwaardelijk verzoek.

Elk item is een sectie: de titel als kop, dan één regel met datum, categorie en eventuele tags, dan de itemtekst zoals die is geschreven, dan de Learn more-link als het item er een heeft, dan de permalink ervan. Het document begint met de titel en beschrijving van je feed en linkt terug naar de gehoste pagina. Als er nog niets is gepubliceerd, staat dat in een zin in plaats van een lege body terug te geven, zodat een lezer dit kan onderscheiden van een mislukt verzoek.

De gehoste pagina kondigt hem aan als rel="alternate"-link met type text/markdown, naast de RSS- en JSON Feed-links, zodat een agent die de HTML heeft geladen hem kan vinden zonder dat het pad hoeft te worden meegedeeld.

Wat wordt aangeboden is de markdown die wij hebben opgesteld en jij hebt goedgekeurd, niet de opgeschoonde HTML. Dat is veilig als markdown, wat inert is, en daarom is dit antwoord nooit text/html. Als je het zelf rendert, escape het dan zoals je elke andere niet-vertrouwde markdown zou escapen: items die zijn opgesteld vanuit een publieke repository kunnen worden beïnvloed door iedereen die daar een pull request kan openen.

Feedback verzamelen op je eigen site

Voeg je origins toe voordat je dit test

Dit is het enige endpoint van het product dat schrijft, dus accepteert het geen verzoeken van zomaar overal. Het vergelijkt de Origin-header van de browser met een whitelist per team, en die lijst begint leeg. Leeg betekent alles weigeren, niet alles toestaan. Tot je de origin toevoegt waarop je insluit, komt elke inzending terug met 403 en {"error":"origin_not_allowed"}, en bereikt niets je inbox. Als je formulier er correct uitziet en toch faalt, is dit bijna altijd de reden. Stel de lijst in met een geauthenticeerde PATCH naar /v1/settings/feed met {"allowedOrigins": ["https://your-site.example"]}, en lees hem terug met een GET naar hetzelfde pad, dat antwoordt met je publicId, je allowedOrigins, en de feedTitle en feedDescription die je abonnees zien in een feedlezer. We slaan elke origin precies op in de vorm waarin een browser die verstuurt, dus een afsluitende schuine streep of een expliciete standaardpoort in wat je verstuurt is geen probleem.

POST/v1/public/YOUR_PUBLIC_ID/feedback
POST https://api.changeloop.dev/v1/public/YOUR_PUBLIC_ID/feedback
Content-Type: application/json
Origin: https://your-site.example

{ "email": "someone@example.com", "message": "Dark mode, please." }

202 Accepted
{ "publicSubmissionId": "0ZbQ8yqk3n7T1sVJ4mWpLd2rXfEuGh6A" }

email moet eruitzien als een e-mailadres en 254 tekens of minder zijn. message mag niet leeg zijn en moet 2 KB of minder wegen, gemeten in UTF-8-bytes, niet in tekens. De JSON-body als geheel is beperkt tot 8 KB. Er is nog een veld, website: dat is een honeypot, laat het dus weg, of stuur het leeg als je het als verborgen invoerveld rendert, zoals onze widget doet.

Het loont om de honeypot te begrijpen voordat je er iets aan debugt. Als website met inhoud binnenkomt, antwoorden we met 202 en een heel gewoon ogende inzend-id en doen we daarna helemaal niets, want een bot die leert dat hij is betrapt, probeert het gewoon anders. Dat is het juiste antwoord voor een bot en een verwarrend antwoord voor jou, dus als je eigen formulier een veld met de naam website heeft dat een browser zou kunnen autofillen, hernoem het dan of laat het weg. Een inzending die geaccepteerd lijkt en nooit verschijnt, is bijna altijd dit.

Een inzending die we accepteren geeft 202 terug met een publicSubmissionId. Geef dat terug aan degene die het heeft verzonden en bewaar het als je kunt: het is de enige manier waarop diegene kan opzoeken wat er daarna is gebeurd.

De foutgevallen zijn 400 met invalid_email of invalid_message bij een verkeerde vorm, 413 met email_too_large of message_too_large bij een correcte vorm maar te veel ervan, 429 met rate_limited voorbij 5 inzendingen per minuut of 30 per uur vanaf één adres tegen één feed, 403 met origin_not_allowed, en 404 met not_found voor een feed-id die we niet herkennen.

Er is ook een dagelijkse limiet per team op hoeveel vervolgwerk inzendingen kunnen veroorzaken. Daarboven blijven we alles nog steeds accepteren en opslaan, het wacht dan alleen tot iemand van je team ernaar kijkt, in plaats van zelf iets te openen.

Een inzending controleren

GET/v1/public/YOUR_PUBLIC_ID/feedback/PUBLIC_SUBMISSION_ID

Antwoordt met status, plus githubIssueUrl zodra er een issue voor die inzending bestaat, plus shippedEntry met een titel en link zodra het werk is uitgeleverd. Het e-mailadres van de verzender wordt voor deze route nooit uit onze database gelezen, laat staan teruggegeven, en dat maakt het antwoord veilig genoeg om te renderen op een pagina die iedereen kan zien. De id is de volledige credential, behandel hem als zodanig. Hij is beperkt tot 20 verzoeken per minuut en 200 per uur per adres en feed.

Cache, CORS en voorwaardelijke verzoeken

Beide feeds sturen Cache-Control: public, max-age=60, stale-while-revalidate=300 samen met een sterke ETag. Stuur die ETag terug als If-None-Match en een ongewijzigde feed antwoordt met 304 zonder body. Geen enkel antwoordveld draagt een klokwaarde, dus de ETag blijft stabiel wanneer we ongewijzigde data opnieuw renderen, en dat maakt die 304's betrouwbaar.

De twee feeds en de statuscontrole zijn anonieme leesbewerkingen en antwoorden met Access-Control-Allow-Origin: *, dus je kunt ze aanroepen vanaf elke origin, met curl of vanuit een build-stap. De feedback-POST is de uitzondering: die antwoordt met je eigen toegestane origin en een Vary: Origin, nooit met een jokerteken. Browsers doen er een preflight voor, en een preflight antwoordt altijd met 204, of de origin nu is toegestaan of niet, dus kan het niet worden gebruikt om je instellingen te verkennen.