Entwicklerdokumentation
Zuletzt aktualisiert am 26. September 2026.
Alles, was Changeloop für Sie veröffentlicht, ist schlichtes JSON über HTTPS. Es gibt kein SDK zu installieren, keinen API-Schlüssel zu rotieren und keinen Anmeldeschritt: Die beiden Feeds unten sind anonyme öffentliche Lesezugriffe, adressiert über Ihre Feed-ID. Ersetzen Sie YOUR_PUBLIC_ID in jedem Beispiel auf dieser Seite durch Ihre eigene.
Eines vorweg: Ihre öffentliche Feed-ID steht in der App selbst. Melden Sie sich an, öffnen Sie die Einstellungen, und sie liegt direkt im Abschnitt „Öffentlicher Feed“, auf dem Sie standardmäßig landen, zusammen mit fertigen Links zu changelog.json und roadmap.json, einem Link auf Ihre gehostete Feed-Seite und dem Widget-Snippet von unten, jeweils mit eigenem Kopier-Knopf.
Erste Schritte
Fünf Schritte führen Sie von der Registrierung zu einem Changelog auf Ihrer eigenen Website. Die Seite „Get started“ in der App führt Sie hindurch und hakt jeden Schritt ab, sobald er erledigt ist.
- Verbinden Sie eine Quelle: ein GitHub-Repository, ein GitLab-Projekt oder ein Bitbucket-Repository.
- Wählen Sie die Sprache, in der Ihre Einträge geschrieben werden.
- Legen Sie optional Tags an, damit Leser nach Produktbereich filtern können.
- Veröffentlichen Sie Ihren ersten Eintrag. Gemergte Änderungen landen als Entwürfe im Review-Postfach: Geben Sie einen frei oder aktivieren Sie die automatische Veröffentlichung für das Repository.
- Bringen Sie das Changelog auf Ihre Website: Verlinken Sie Ihre gehostete Seite, fügen Sie das Widget ein oder rendern Sie den JSON-Feed auf Ihrer eigenen Seite.
Ihr Changelog in etwa zehn Zeilen React
Fügen Sie das in eine Komponente ein, und Sie haben ein funktionierendes Changelog. Mehr ist nicht nötig.
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 ist das Markdown, das wir entworfen haben, als Text. Wenn Sie lieber formatierte Ausgabe rendern, nehmen Sie stattdessen htmlContent: Es wird serverseitig von unserem eigenen Sanitiser aus einer festen Positivliste von Tags und Attributen gebaut und ist der einzige Wert in all diesen Antworten, der als Markup eingefügt werden darf. Alles andere ist Text, und Einträge, die aus einem öffentlichen Repository entworfen wurden, kann jede Person beeinflussen, die dort einen Pull Request öffnen kann. Behandeln Sie sie entsprechend.
Der Changelog-Feed
GET/v1/public/YOUR_PUBLIC_ID/changelog.jsonIhre veröffentlichten Einträge, neueste zuerst, wobei bei identischen Zeitstempeln die neuere id den Ausschlag gibt.
Query-Parameter
- repos nimmt eine kommagetrennte Liste vollständiger Repository-Namen, zum Beispiel acme/web,acme/api. Es kommen nur Einträge aus diesen Repositories zurück. Lassen Sie es weg, erhalten Sie alle.
- limit ist die Anzahl der Einträge pro Seite. Der Standardwert ist 20, alles über 50 wird auf 50 begrenzt, und alles, was sich nicht als positive Zahl lesen lässt, fällt auf 20 zurück, statt einen Fehler zu erzeugen.
- cursor ist opak. Nehmen Sie den Wert nextCursor aus der vorherigen Antwort und geben Sie ihn unverändert zurück. Ein Cursor, den wir nicht dekodieren können, wird wie kein Cursor behandelt, Sie erhalten also die erste Seite erneut statt einen Fehler.
Antwort
{
"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" }
}
Jeder Eintrag trägt dieselben neun Schlüssel: id, title, mdContent, htmlContent, repoFullName, category, tags, learnMoreUrl und publishedAt. category ist einer von feature, fix oder internal und ist null, wenn beim Entwurf keiner gesetzt wurde, publishedAt ist eine ISO-8601-Zeichenkette, und htmlContent ist ein leerer String bei einem Eintrag, der nie durch den Entwurf gelaufen ist. tags ist ein Array Ihrer eigenen Produktbereichsnamen und ist leer, wenn keine zugewiesen wurden, learnMoreUrl ist null, sofern es beim Review niemand ergänzt hat, und die Farbe für jedes Tag stammt aus der tagColors-Map der Antwort und nicht aus dem Eintrag, sodass ein Tag, das Sie inzwischen aus Ihrem Vokabular entfernt haben, schlicht ohne Farbe gerendert wird. nextCursor ist null, wenn Sie das Ende erreicht haben.
Eine unbekannte Feed-ID antwortet mit 404 und {"error":"not_found"}, eine fehlerhafte ebenso. Die beiden sind bewusst nicht unterscheidbar, damit sich über diesen Endpunkt nicht ermitteln lässt, welche IDs existieren.
Der Roadmap-Feed
GET/v1/public/YOUR_PUBLIC_ID/roadmap.jsonDieselben drei Spalten, die Ihr Team von Hand pflegt.
{
"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 ist ein Array, kein nach Spaltennamen indiziertes Objekt, und seine Reihenfolge ist Teil des Vertrags: planned, dann building, dann shipped. Alle drei sind immer vorhanden, leere eingeschlossen, sodass Sie „Spalte gibt es nicht“ nie von „noch nichts darin“ unterscheiden müssen. Rendern Sie sie in der empfangenen Reihenfolge, dann stimmen Sie mit jeder anderen Oberfläche überein, die wir bauen.
Ein Item hat genau fünf Schlüssel: id, column, publicTitle, publicDescription und publishedAt. publicDescription ist immer eine Zeichenkette und kann leer sein, nie null. Nichts über das Issue, aus dem ein Item stammt, wird hier offengelegt, weder das Repository noch die Issue-Nummer, und das ist Absicht und kein Versäumnis, das wir später nachreichen.
Dieser Endpunkt nimmt überhaupt keine Query-Parameter. Es gibt keinen Cursor, kein limit und keinen Repository-Filter, denn eine Roadmap ist ein kleines Board, das ein Mensch kuratiert, und kein Log, das endlos wächst. Jede Spalte liefert bis zu 50 Items und setzt hasMore, wenn es mehr waren. hasMore ist informativ: Es gibt keinen Cursor, dem man folgen könnte, bauen Sie also keine Blätterfunktion darum.
publicTitle und publicDescription sind einfacher Text, entworfen aus Issue-Titeln und -Inhalten, die in einem öffentlichen Repository jede Person durch das Öffnen eines Issues beeinflussen kann. Sie tragen keine Zusicherung zur HTML-Bereinigung und sind nicht die htmlContent-Ausnahme. Rendern Sie sie als Text.
Das einbettbare Widget
Wenn Sie lieber nichts bauen möchten, fügen Sie diese zwei Zeilen ein. Das Widget ist ein Custom Element, das in einen Shadow Root rendert und dadurch weder Ihre Styles erbt noch in sie hineinwirkt.
<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 Attribute sind erforderlich. data-public-id ist Ihre Feed-ID, data-api ist der Ursprung, von dem das Widget lädt. Fehlt eines von beiden, schreibt das Element einen Fehler in die Konsole und rendert gar nichts, was das Erste ist, das Sie prüfen sollten, wenn Sie eine leere Stelle sehen, wo es stehen müsste.
Fügen Sie dem Element data-theme="dark" hinzu, um es dunkel zu rendern; Ihre Seite kann das zur Laufzeit umschalten. Für tiefergehendes Styling stellt das Widget CSS Custom Properties (--changelogapp-text, --changelogapp-bg, --changelogapp-accent und weitere) sowie ::part()-Namen bereit, die Sie in Ihrem eigenen Stylesheet setzen. Die App zeigt beide Designs live unter Einstellungen, Öffentlicher Feed.
Fügen Sie data-repos hinzu, um nur einige Ihrer Repositories anzuzeigen, zum Beispiel das Changelog eines Produkts auf der Website dieses Produkts, wenn sich mehrere Produkte ein Konto teilen. Der Wert ist eine kommagetrennte Liste vollständiger Namen, owner/repo; ein Name ohne Besitzer passt auf nichts und rendert einen leeren Feed ohne Fehler. Bis zu zehn Repositories werden berücksichtigt. Ein so eingegrenztes Widget zeigt nur Updates und Feedback, weil die Roadmap keine Ansicht pro Repository hat, und Feedback wird weiterhin dort abgelegt, wohin das Feedback-Ziel Ihres Teams zeigt. Unter Einstellungen, Öffentlicher Feed gibt es eine Auswahl, die das Attribut für Sie schreibt.
Es rendert drei Tabs in dieser Reihenfolge: Updates, Roadmap und Feedback. Die ersten beiden lesen die Feeds von oben. Der dritte sendet an den Endpunkt unten und legt jede Submission-ID im localStorage ab, sodass Besucherinnen und Besucher zurückkommen und sehen können, was aus ihrer Meldung geworden ist.
Das Skript wird versioniert ausgeliefert. /widget.js liefert immer den neuesten Build und wird eine Stunde lang gecacht, sodass ein Release Ihre Besucher erreicht, ohne dass Sie etwas anfassen. /widget-vN.js pinnt einen Build: Sobald eine Versionsnummer ausgeliefert wurde, ändern sich ihre Bytes nie wieder, und sie wird ein Jahr lang gecacht. Pinnen Sie sie, wenn Sie Änderungen lieber bewusst übernehmen.
Laden Sie genau ein Widget-Skript pro Seite
Die beiden URLs sind Alternativen, keine Schichten. Beide registrieren denselben Namen für das Custom Element, und ein Browser lässt einen Namen pro Dokument nur einmal registrieren: Welches Skript zuerst ausgeführt wird, gewinnt für die Lebensdauer der Seite, das zweite bleibt wirkungslos. Eine Seite, die sowohl /widget.js als auch /widget-v5.js trägt, rendert also das, was der Browser zufällig zuerst ausgeführt hat, und das haben Sie nicht in der Hand; /widget-v5.js neben ein vorhandenes /widget.js zu setzen, um die Version zu pinnen, bewirkt gar nichts. Meist gewinnt der ältere Build, weil er schon im Cache liegt.
Wenn das passiert, schreibt das Widget eine Warnung mit beiden Build-Namen in die Konsole, damit Sie nicht raten müssen. Mehr als warnen kann es nicht: Wenn die zweite Kopie läuft, hat die erste den Namen längst beansprucht. Die Lösung ist immer, das Skript-Tag zu ersetzen statt ein weiteres hinzuzufügen, und dasselbe gilt, wenn ein Tag-Manager oder ein Partial eines für Sie einfügt. Um vom rollierenden auf einen gepinnten Build zu wechseln, ändern Sie das src.
Die gehostete Feed-Seite
https://feed.changeloop.dev/feed/YOUR_PUBLIC_IDUnter dieser Adresse hosten wir auch eine schlichte Seite: Ihr Changelog und Ihr Roadmap-Board, gerendert aus denselben beiden Feeds von oben. Sie braucht keine Anmeldung und auf Ihrer Seite keine Einrichtung. Dorthin schicken wir auch Menschen zurück, sobald sich eine Schleife schließt: Der Shipped-Kommentar, den wir an einem GitHub-Issue hinterlassen, verlinkt hierher, ebenso shippedEntry.link aus der Statusabfrage oben, und beide landen auf dem ausgelieferten Eintrag mit eigenem #entry-ID-Anker, der den Eintrag auch dann noch findet, wenn er inzwischen auf eine spätere Seite gerutscht ist.
Betrachten Sie sie als Rückfalloption, nicht als die Integration. Der Changelog-Feed und das Widget bleiben der Weg, das in Ihre eigene Website zu bringen, sodass es nach Ihrem Produkt aussieht und nicht nach unserem; diese Seite ist für die Zeit davor da und für Loop-Close-Links, die unabhängig davon hierher zeigen, was Sie sonst gebaut haben.
Ihre eigene Domain
Sie können die gehostete Seite unter Ihrer eigenen Adresse ausliefern, ohne DNS- oder Zertifikatsänderungen. Fügen Sie unter Einstellungen, Eigene Domain die öffentliche Adresse ein, die Ihre Leser sehen (zum Beispiel https://example.com/changelog), und leiten Sie diesen Pfad auf Ihrer Website an das dort angezeigte Proxy-Ziel weiter: eine Regel deckt die Seite, ihre Assets, ihre Daten und ihre Feeds ab. Domain prüfen ruft Ihre Adresse von unserer Seite aus ab und sagt Ihnen, ob der Proxy stimmt und, falls nicht, was zu ändern ist.
Der MCP-Server
POSThttps://api.changeloop.dev/mcpWenn Sie in Claude Code, ChatGPT oder einem anderen Agenten arbeiten, der das Model Context Protocol spricht, können Sie ihn direkt mit Ihrem Changelog verbinden. Der Agent sieht dann, was zur Durchsicht wartet, kann die Formulierung bearbeiten und veröffentlichen, ohne dass Sie den Editor verlassen. Es ist dasselbe Review-Tor wie in der Web-App: Nichts wird öffentlich, bevor etwas es freigibt.
Claude Code verbinden
Legen Sie zuerst einen API-Schlüssel an (Einstellungen, API-Schlüssel) und fügen Sie dann den Server mit Ihrem Schlüssel im Header hinzu:
claude mcp add --transport http changeloop \
https://api.changeloop.dev/mcp \
--header "Authorization: Bearer clapi_YOUR_KEY"
Für einen Client, der stattdessen eine JSON-Konfiguration liest, sieht dasselbe so aus:
{
"mcpServers": {
"changeloop": {
"type": "http",
"url": "https://api.changeloop.dev/mcp",
"headers": { "Authorization": "Bearer clapi_YOUR_KEY" }
}
}
}
Einen OAuth-Ablauf gibt es noch nicht. Die Authentifizierung ist der API-Schlüssel im Header, und genau das tun die beiden Befehle oben. Widerrufen Sie diesen Schlüssel in den Einstellungen, trennt sich der Agent bei seiner nächsten Anfrage.
Was der Agent tun kann
Sieben Werkzeuge, und die Liste ist bewusst kurz. Alles Weitere, was dieses Produkt kann, ist über die REST-API mit demselben Schlüssel erreichbar; jedes Werkzeug, das einem Agenten offensteht, ist eine weitere Sache, zu deren Aufruf er sich überreden lässt.
- list_pending_entries, list_published_entries, get_entry - lesen Ihre Einträge. Ausstehende sind nicht öffentlich.
- update_entry - ändert Titel oder Markdown-Text eines Eintrags. Das HTML, das der Feed ausliefert, wird von unserem Sanitiser aus Ihrem Markdown neu erzeugt; ein Agent kann kein HTML liefern.
- approve_entry - veröffentlichen. Das ist öffentlich und sofort wirksam und benachrichtigt verknüpftes Feedback auf GitHub. Nur ein ausstehender Eintrag lässt sich freigeben.
- discard_entry - hält einen Eintrag aus dem Changelog heraus. Aus der Web-App umkehrbar.
- get_changelog_info - Ihre Feed-ID und die Adressen, unter denen Ihr Changelog ausgeliefert wird.
Was er nicht kann
Jedes Werkzeug ist auf das Team begrenzt, zu dem der Schlüssel gehört, und keines nimmt ein Team als Argument, es gibt also nichts, womit man auf ein anderes Team zeigen könnte, selbst wenn es jemand versuchte. Der Server akzeptiert keine Browser-Sitzung, nur einen Schlüssel: Eine Anfrage muss das Zugangsmerkmal bewusst mitschicken. Und ein Schlüssel kann keine Schlüssel verwalten und Ihren Datenexport nicht herunterladen, ein so verbundener Agent kann sich also kein zweites Zugangsmerkmal ausstellen und Ihre Daten nicht in einem Aufruf herausziehen.
API-Schlüssel
Alles bisher Genannte ist anonym und braucht kein Zugangsmerkmal. Die authentifizierte API, also Ihre Einstellungen und Ihr Review-Postfach, ist eine andere Oberfläche, und sie akzeptiert entweder eine angemeldete Browser-Sitzung oder einen API-Schlüssel. Schlüssel sind für Skripte und Agenten gedacht: für alles, was Ihr Changelog ohne Menschen an der Tastatur erreichen muss.
Authorization: Bearer clapi_YOUR_KEYLegen Sie einen in der App unter Einstellungen im Tab „API-Schlüssel“ an. Der Schlüssel wird genau einmal angezeigt, im Moment der Erstellung, und nie wieder: Wir speichern nur einen Hash davon, es gibt also keine Ansicht, die ihn Ihnen ein zweites Mal zeigen könnte. Wenn Sie ihn verlieren, widerrufen Sie ihn und legen einen neuen an.
Was ein Schlüssel kann und was nicht
Ein Schlüssel trägt denselben Zugriff wie eine Anmeldung, begrenzt auf das eine Team, in dem er erstellt wurde, mit zwei bewussten Ausnahmen. Er kann keine API-Schlüssel verwalten, und er kann Ihren Datenexport nicht herunterladen. Beides erfordert eine echte Anmeldung, damit ein Schlüssel, der abhandenkommt, sich keinen Ersatz ausstellen, nicht die Schlüssel widerrufen kann, mit denen Sie ihn aussperren würden, und Ihre Teamdaten nicht in einer einzigen Anfrage herausziehen kann.
Widerrufen
Ein Widerruf greift bei der nächsten Anfrage. Ein widerrufener Schlüssel antwortet mit 401, genau wie ein unbekannter, und er antwortet auch dann weiter mit 401, wenn der Browser noch eine gültige Sitzung hält, denn eine Anfrage mit Authorization-Header wird nie stillschweigend als Cookie-Anfrage wiederholt. Der widerrufene Schlüssel bleibt gelistet, mit dem Datum des Widerrufs und dem der letzten Nutzung, und genau das braucht man, wenn man herausfinden will, was ein abhandengekommener Schlüssel erreicht hat.
Pläne und Limits
Der kostenlose Plan schreibt 20 gemergte Änderungen pro Monat auf und begrenzt die tägliche Zahl geprüfter Merges, triagierter Feedback-Einsendungen, entworfener Roadmap-Karten und alternativer Versionen auf jeweils 50; der Team-Plan hat keine harten Limits. Einstellungen, Plan und Nutzung zeigt jedes Budget so, wie das Produkt es selbst zählt, mit dem Zeitpunkt, an dem es zurückgesetzt wird, bevor etwas abgelehnt wird. Arbeit, die über einem Limit eintrifft, wird zurückgehalten, nicht verworfen: ein Eintrag über der Quote wartet im Posteingang, und ein abgelehnter Roadmap-Entwurf kann erneut versucht werden, sobald das Fenster weiterrückt.
GitLab und Bitbucket
Ein GitLab-Projekt oder ein Bitbucket-Repository kann Ihr Changelog genauso speisen wie ein GitHub-Repository: unter Einstellungen, dann GitLab, oder Einstellungen, dann Bitbucket, verbinden, den Webhook eintragen, den wir Ihnen geben (oder ihn auf bitbucket.org von Connect with Bitbucket anlegen lassen, wenn die Bitbucket-Seite diese Schaltfläche anbietet), und jede Änderung, die in den von Ihnen genannten Branch gemergt wird, wird zu einem Entwurfseintrag in Ihrem Review-Postfach, gleich geschrieben und durch dieselbe menschliche Durchsicht abgesichert. Einträge entstehen aus gemergten Pull Requests oder Merge Requests oder, bei GitHub und Bitbucket, aus Pushes, wenn Sie unter Einstellungen, dann What creates drafts, den Push-Modus wählen. GitLab-Projekte erzeugen Entwürfe nur aus Merge Requests.
Ein Projekt verbinden
GitLab-Projekte verbinden Sie unter Einstellungen, dann GitLab, Bitbucket-Repositorys unter Einstellungen, dann Bitbucket. Geben Sie den Pfad ein (bei GitLab Gruppe und Projekt, etwa acme/web, bei Bitbucket Workspace und Repository, etwa acme/app), und wir geben Ihnen eine Webhook-Adresse und ein Secret zurück. Fügen Sie beides in die Webhook-Einstellungen auf der Gegenseite ein: bei GitLab „Merge request events“ ankreuzen, bei Bitbucket die Trigger „Merged pull request“ und „Push repository“ ankreuzen. Selbst betriebene Instanzen funktionieren, über https. Das Secret wird genau einmal angezeigt, in diesem Moment. Wenn Sie es verlieren, entfernen Sie das Projekt und verbinden es erneut. Auf bitbucket.org, wenn die Bitbucket-Seite eine Schaltfläche Connect with Bitbucket anzeigt, können Sie sich das Einfügen sparen: Klicken Sie darauf, erlauben Sie den Zugriff einmal, und wir lesen den Haupt-Branch des Repositorys aus und legen den Webhook für Sie an. Dafür brauchen Sie Admin-Rechte am Repository. Bei selbst betriebenem Bitbucket oder wenn Sie lieber selbst einfügen, wählen Sie Set it up by hand und erhalten Adresse und Secret wie oben. Wenn Sie ein Bitbucket-Repository entfernen und neu verbinden, löschen Sie auch den alten Webhook auf Bitbucket, unter Repository settings, dann Webhooks. Sobald ein Projekt verbunden ist, können Sie in seiner Zeile den Branch ändern und automatisches Veröffentlichen einschalten, und wenn eine Zustellung ignoriert wurde, nennt die Zeile den Grund.
Warum Bitbucket nach einem Branch fragt und GitLab nicht
GitLab teilt uns mit, welchen Branch Ihr Projekt als Standard behandelt, Sie können das Feld also leer lassen und genau das meinen. Bitbucket sendet überhaupt keinen Standard-Branch, wenn wir das Feld dort leer ließen, hätten wir nichts zum Vergleichen, und Ihr Webhook säße da und sähe perfekt installiert aus, ohne je einen einzigen Eintrag zu erzeugen. Da stellen wir lieber eine Frage. Mit Connect with Bitbucket fragen wir Bitbucket nach dem Haupt-Branch, sobald Sie den Zugriff erlauben, sodass Sie ihn nicht eintippen müssen.
Was sie noch nicht abdecken
Changelog-Einträge, und sonst nichts. Dass das Feedback-Widget für Sie ein Issue anlegt, dass die Antwort auf diesem Issue gepostet wird, sobald der Fix draußen ist, dass die öffentliche Roadmap aus Issue-Labels gespeist wird und dass es im Review-Postfach eine Quellvorschau gibt, ist heute alles GitHub-exklusiv.
Den Grund nennen wir lieber, als ihn zu übertünchen. Jedes dieser Dinge braucht ein Zugriffstoken mit Schreibrechten auf Ihr Projekt, das bei uns liegt. Changelog-Einträge brauchen keines, weil alles, woraus sie geschrieben werden, im Webhook selbst ankommt; GitLab oder Bitbucket per Webhook zu verbinden gibt uns also kein Zugangsmerkmal und keinen Lesezugriff auf Ihren Code. Connect with Bitbucket ist die eine Ausnahme. Bitbucket leiht uns für eine einzige Anfrage ein Token, mit dem sich das Repository und seine Pull Requests lesen und seine Webhooks verwalten lassen. Wir nutzen es nur, um den Haupt-Branch zu lesen und den Webhook anzulegen, und verwerfen es danach. Gespeichert wird nichts. Wir liefern lieber den Teil aus, der Sie nichts kostet, als für eine runde Funktionsliste nach einem Token zu fragen.
Weitere Fassungen eines Eintrags
Eine Änderung muss meist mehr als einmal erklärt werden: den Kundinnen und Kunden im Changelog, denjenigen, die Fragen dazu beantworten, und in einem Kanal, in dem niemand vier Absätze liest. Aus dem Review-Postfach heraus können Sie vor der Freigabe zwei zusätzliche Fassungen eines Eintrags entwerfen.
Eine Ankündigungsfassung ist ein bis zwei Zeilen und wird bei der Freigabe anstelle des vollen Texts nach Slack gepostet. Eine Support-Notiz ist ein internes Briefing: was sich geändert hat, was Kundinnen und Kunden merken werden, und ein Satz, den eine Support-Person fast wörtlich sagen könnte. Beides sind Entwürfe, die Sie vor der Verwendung überarbeiten können, und beides lässt sich entfernen.
Veröffentlicht wird keine von beiden
Diese Fassungen erscheinen nie auf Ihrer Changelog-Seite, in keinem Feed, nicht im Widget und nicht über die API, die sie ausliefert. Besonders die Support-Notiz ist für Menschen in Ihrem Unternehmen geschrieben und kann deutlicher ausfallen als der Eintrag selbst. Sie existiert nur in Ihrem Review-Postfach und, falls Sie sie verwenden, in Ihrer eigenen Kopie.
Woraus sie geschrieben werden
Immer aus dem Eintrag, nie aus dem Pull Request. Das ist Absicht: Der Eintrag ist bereits durch die Regel gelaufen, die Sicherheitsfixes vage hält, und durch Ihre eigene Durchsicht. Eine daraus umgeschriebene Fassung kann ein Detail, das Sie entfernt haben, nicht wieder hereinbringen, weil das Detail in dem, was das Modell bekommen hat, nicht enthalten ist.
In Slack ankündigen
Geben Sie einen Eintrag frei, kann er im selben Moment, in dem er öffentlich wird, in einen Slack-Kanal gepostet werden. Verbinden Sie das unter Einstellungen im Tab „Slack“: Legen Sie in Ihrem eigenen Workspace einen Incoming Webhook an, wählen Sie den Kanal und fügen Sie die URL ein. Über diesen Webhook hinaus wird bei Ihnen nichts installiert, und wir verlangen keinen Zugriff auf Ihren Workspace.
Die Nachricht trägt den Eintragstitel, den Text in der von Ihnen freigegebenen Fassung, Kategorie und Tags sowie einen Link zurück auf den Eintrag in Ihrem Changelog. Markdown wird in das übersetzt, was Slack tatsächlich rendert, ein Eintrag kommt also nicht mit sichtbaren Sternchen an.
Die Webhook-URL ist ein Zugangsmerkmal
Wer diese URL hat, kann in den Kanal posten, wir behandeln sie deshalb wie ein Passwort: Sie wird gespeichert, und danach zeigt sie keine Ansicht und keine API-Antwort je wieder an, auch Ihr eigener Datenexport nicht. Was Sie danach sehen, ist eine Maskierung, die ausreicht, um zwei Webhooks auseinanderzuhalten, und für alle anderen nutzlos ist. Wir akzeptieren ausschließlich eine hooks.slack.com-Adresse, eine vertippte oder untergeschobene URL wird also abgelehnt statt abgerufen.
Wenn es nicht mehr funktioniert
Entfernen Sie die App in Slack oder archivieren Sie den Kanal, funktioniert der Webhook dauerhaft nicht mehr. Wir bemerken das bei der ersten abgelehnten Nachricht, schalten Ankündigungen ab und sagen es im Slack-Tab mit Grund und Datum. Dass wir es nicht still weiter versuchen, ist Absicht: Ein Changelog, das niemand angekündigt hat, sieht genauso aus wie eines, das niemand gelesen hat, und dieser Unterschied ist es wert, mitgeteilt zu werden.
Pausieren
Pausieren stoppt die Ankündigungen und behält den Webhook, das Fortsetzen ist also ein Klick statt eines weiteren Wegs durch Slack. Trennen entfernt die URL vollständig. In beiden Fällen bleibt das Veröffentlichen selbst unberührt: Slack ist ein Kanal, in den Ihr Changelog postet, nie ein Tor, auf das es wartet. Ist Slack bei der Freigabe nicht erreichbar, wird der Eintrag trotzdem veröffentlicht und die Ankündigung von selbst erneut versucht.
RSS und JSON Feed
GEThttps://api.changeloop.dev/v1/public/YOUR_PUBLIC_ID/rss.xmlGEThttps://api.changeloop.dev/v1/public/YOUR_PUBLIC_ID/feed.jsonDieselben veröffentlichten Einträge als abonnierbarer Feed, in den beiden Formaten, die Reader verstehen: RSS 2.0 und JSON Feed 1.1. Beide nehmen dieselben repos-, category- und tag-Filter wie der Changelog-Feed und tragen dasselbe Cache-Control und ETag. Keiner von beiden blättert: Ein Reader fragt den Kopf des Feeds ab, diese liefern also nur die neuesten Einträge, ohne Cursor.
Der Eintragstext ist das bereinigte HTML, bei RSS in CDATA verpackt und bei JSON Feed als content_html. JSON Feed trägt zusätzlich Ihre Tag-Farben unter einer namensraumbezogenen _changelogapp-Erweiterung; RSS nicht, weil kein Reader sie zeichnen würde.
Die gehostete Seite weist beide als rel="alternate"-Links aus, sodass ein Browser oder Reader, der dort landet, abonnieren kann, ohne die Pfade zu kennen.
Ein einzelner Eintrag
GEThttps://api.changeloop.dev/v1/public/YOUR_PUBLIC_ID/entries/ENTRY_IDLiefert einen einzelnen veröffentlichten Eintrag, dasselbe Objekt, das der Changelog-Feed in seinem data-Array trägt. Darauf zeigen die Permalinks in den Feeds, und es ist nützlich, wenn Sie eine id haben und nicht durch den Feed blättern möchten, um sie zu finden. Eine unbekannte id, oder eine zu einem nicht veröffentlichten Eintrag, liefert 404 mit demselben Body wie jede andere unbekannte id.
Der Markdown-Feed
GEThttps://api.changeloop.dev/v1/public/YOUR_PUBLIC_ID/changelog.mdDieselben veröffentlichten Einträge als reines Markdown, ausgeliefert als text/markdown. Es gibt ihn für Leser, die keine Browser sind: Ein LLM oder ein Agent, der beantwortet, was sich zuletzt an diesem Produkt geändert hat, bekommt den Text, ohne RSS zu parsen oder JSON zu durchlaufen. Er nimmt dieselben repos-, category- und tag-Filter wie der Changelog-Feed, trägt dasselbe Cache-Control und ETag und antwortet auf eine bedingte Anfrage genauso mit 304 wie die anderen beiden.
Jeder Eintrag ist ein Abschnitt: der Titel als Überschrift, dann eine einzelne Zeile mit Datum, Kategorie und etwaigen Tags, dann der Eintragstext, wie er geschrieben wurde, dann der Learn-more-Link, falls der Eintrag einen hat, dann sein Permalink. Das Dokument beginnt mit Titel und Beschreibung Ihres Feeds und verlinkt zurück auf die gehostete Seite. Ist noch nichts veröffentlicht, sagt es das in einem Satz, statt einen leeren Body zu liefern, damit ein Leser das von einem fehlgeschlagenen Abruf unterscheiden kann.
Die gehostete Seite weist ihn als rel="alternate"-Link mit type text/markdown aus, neben den RSS- und JSON-Feed-Links, sodass ein Agent, der das HTML geladen hat, den Pfad findet, ohne ihn genannt zu bekommen.
Ausgeliefert wird das Markdown, das wir entworfen haben und das Sie freigegeben haben, nicht das bereinigte HTML. Als Markdown ist das unbedenklich, denn Markdown ist inert, und genau deshalb ist diese Antwort nie text/html. Wenn Sie es selbst rendern, escapen Sie es so, wie Sie jedes andere nicht vertrauenswürdige Markdown escapen würden: Einträge, die aus einem öffentlichen Repository entworfen wurden, kann jede Person beeinflussen, die dort einen Pull Request öffnen kann.
Feedback auf Ihrer eigenen Website sammeln
Tragen Sie Ihre Origins ein, bevor Sie das testen
Das ist der einzige Endpunkt im Produkt, der schreibt, deshalb nimmt er nicht von überall Anfragen an. Er gleicht den Origin-Header des Browsers gegen eine Positivliste pro Team ab, und diese Liste ist anfangs leer. Leer bedeutet: alles ablehnen, nicht alles erlauben. Bis Sie den Origin eintragen, auf dem Sie einbetten, kommt jede einzelne Übermittlung mit 403 und {"error":"origin_not_allowed"} zurück, und nichts erreicht Ihr Postfach. Wenn Ihr Formular korrekt aussieht und trotzdem scheitert, ist fast immer das der Grund. Setzen Sie die Liste mit einem angemeldeten PATCH auf /v1/settings/feed mit {"allowedOrigins": ["https://your-site.example"]} und lesen Sie sie mit einem GET auf denselben Pfad zurück, der Ihnen publicId, allowedOrigins sowie feedTitle und feedDescription liefert, die Ihre Abonnenten im Feed-Reader sehen. Wir speichern jeden Origin genau in der Form, in der ein Browser ihn sendet, ein abschließender Schrägstrich oder ein ausgeschriebener Standard-Port in Ihrer Eingabe ist also unproblematisch.
POST/v1/public/YOUR_PUBLIC_ID/feedbackPOST 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 muss wie eine E-Mail-Adresse aussehen und höchstens 254 Zeichen lang sein. message muss nicht leer sein und höchstens 2 KB groß, gemessen in UTF-8-Bytes statt in Zeichen. Der JSON-Body insgesamt ist auf 8 KB begrenzt. Es gibt ein weiteres Feld, website: Das ist ein Honeypot, lassen Sie es also weg oder senden Sie es leer, wenn Sie es als verstecktes Eingabefeld rendern, wie unser Widget es tut.
Den Honeypot sollte man verstanden haben, bevor man irgendetwas daran debuggt. Kommt website mit Inhalt an, antworten wir mit 202 und einer völlig gewöhnlich aussehenden Submission-ID und tun danach gar nichts, denn ein Bot, der merkt, dass er erwischt wurde, versucht es einfach anders. Für einen Bot ist das die richtige Antwort und für Sie eine verwirrende, wenn also Ihr eigenes Formular ein Feld namens website hat, das ein Browser automatisch ausfüllen könnte, benennen Sie es um oder lassen Sie es weg. Eine Übermittlung, die angenommen wirkt und nie auftaucht, ist fast immer das.
Eine angenommene Übermittlung liefert 202 mit einer publicSubmissionId. Geben Sie diese an die absendende Person zurück und bewahren Sie sie nach Möglichkeit auf: Sie ist die einzige Möglichkeit, nachzusehen, was daraus geworden ist.
Die Fehlerfälle sind 400 mit invalid_email oder invalid_message bei falscher Form, 413 mit email_too_large oder message_too_large bei richtiger Form, aber zu viel davon, 429 mit rate_limited jenseits von 5 Übermittlungen pro Minute oder 30 pro Stunde von einer Adresse gegen einen Feed, 403 mit origin_not_allowed und 404 mit not_found für eine Feed-ID, die wir nicht kennen.
Zusätzlich gibt es pro Team eine Tagesobergrenze dafür, wie viel nachgelagerte Arbeit Übermittlungen auslösen können. Darüber hinaus nehmen wir weiterhin alles an und speichern es, es wartet dann nur darauf, dass jemand aus Ihrem Team hineinschaut, statt von selbst etwas zu öffnen.
Eine einzelne Übermittlung nachschlagen
GET/v1/public/YOUR_PUBLIC_ID/feedback/PUBLIC_SUBMISSION_IDAntwortet mit status, dazu githubIssueUrl, sobald es zu dieser Übermittlung ein Issue gibt, und shippedEntry mit Titel und Link, sobald die Arbeit draußen ist. Die E-Mail-Adresse der absendenden Person wird für diese Route nie aus unserer Datenbank gelesen, geschweige denn zurückgegeben, und genau das macht die Antwort sicher genug, um sie auf einer öffentlich sichtbaren Seite zu rendern. Die ID ist das gesamte Zugangsmerkmal, behandeln Sie sie entsprechend. Sie ist auf 20 Anfragen pro Minute und 200 pro Stunde je Adresse und Feed begrenzt.
Caching, CORS und bedingte Anfragen
Beide Feeds senden Cache-Control: public, max-age=60, stale-while-revalidate=300 zusammen mit einem starken ETag. Schicken Sie dieses ETag als If-None-Match zurück, dann antwortet ein unveränderter Feed mit 304 ohne Body. Kein Antwortfeld trägt einen Uhrzeitwert, das ETag bleibt also stabil, wenn wir unveränderte Daten neu rendern, und genau deshalb kann man sich auf diese 304er verlassen.
Die beiden Feeds und die Statusabfrage sind anonyme Lesezugriffe und antworten mit Access-Control-Allow-Origin: *, Sie können sie also von jedem Origin, per curl oder aus einem Build-Schritt aufrufen. Der Feedback-POST ist die Ausnahme: Er antwortet mit Ihrem eigenen erlaubten Origin und einem Vary: Origin, nie mit einem Wildcard. Browser schicken dafür einen Preflight, und ein Preflight antwortet immer mit 204, unabhängig davon, ob der Origin erlaubt ist, er lässt sich also nicht nutzen, um Ihre Einstellungen auszuspähen.