Vai al contenuto

Documentazione per sviluppatori

Ultimo aggiornamento: 26 settembre 2026.

Tutto ciò che Changeloop pubblica per te è JSON semplice su HTTPS. Non c'è nessun SDK da installare, nessuna chiave API da ruotare e nessun passaggio di accesso: i due feed qui sotto sono letture pubbliche anonime identificate dal tuo id feed. Sostituisci YOUR_PUBLIC_ID con il tuo in qualsiasi esempio di questa pagina.

Una cosa da sapere prima di iniziare: il tuo id feed pubblico si trova nell'app stessa. Accedi, apri Impostazioni, ed è proprio nella sezione Feed pubblico, quella in cui atterri per impostazione predefinita, insieme a link già pronti a changelog.json e roadmap.json, un link alla tua pagina di feed ospitata e lo snippet del widget qui sotto, ciascuno con il proprio pulsante di copia.

Per iniziare

Cinque passaggi ti portano dalla registrazione a un changelog sul tuo sito. La pagina «Get started» nell'app ti guida passo passo e spunta ogni passaggio appena lo completi.

  1. Collega una sorgente: un repository GitHub, un progetto GitLab o un repository Bitbucket.
  2. Scegli la lingua in cui vengono scritte le tue voci.
  3. Se vuoi, crea delle etichette così chi legge può filtrare per area del prodotto.
  4. Pubblica la tua prima voce. Le modifiche unite arrivano come bozze nella casella di revisione: approvane una o attiva la pubblicazione automatica per quel repository.
  5. Mettilo sul tuo sito: inserisci un link alla tua pagina ospitata, incolla il widget oppure mostra il feed JSON nella tua pagina.

Il tuo changelog in circa dieci righe di React

Incolla questo in un componente e hai un changelog funzionante. Non c'è altro da aggiungere.

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 è il markdown che abbiamo redatto, come testo. Se preferisci renderizzare un output formattato, usa htmlContent: viene costruito lato server dal nostro sanitizzatore a partire da una lista fissa di tag e attributi consentiti, ed è l'unico valore in tutte queste risposte pensato per essere inserito come markup. Tutto il resto è testo, e le voci redatte da un repository pubblico possono essere influenzate da chiunque possa aprirvi una pull request, quindi trattale di conseguenza.

Il feed del changelog

GET/v1/public/YOUR_PUBLIC_ID/changelog.json

Le tue voci pubblicate, dalla più recente, con l'id più recente a decidere i pareggi su timestamp identici.

Parametri di query

  • repos accetta un elenco separato da virgole di nomi completi di repository, ad esempio acme/web,acme/api. Tornano solo le voci di quei repository. Omettilo e le otterrai tutte.
  • limit è quante voci vuoi per pagina. Il valore predefinito è 20, qualsiasi valore sopra 50 viene limitato a 50, e qualsiasi valore che non possiamo leggere come numero positivo torna a 20 invece di fallire.
  • cursor è opaco. Prendi il valore nextCursor dalla risposta precedente e restituiscilo tale e quale. Un cursore che non possiamo decodificare viene trattato come nessun cursore, quindi ottieni di nuovo la prima pagina invece di un errore.

Risposta

{
  "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" }
}

Ogni voce ha le stesse nove chiavi: id, title, mdContent, htmlContent, repoFullName, category, tags, learnMoreUrl e publishedAt. category è feature, fix o internal, ed è null quando chi ha redatto non ne ha impostata una, publishedAt è una stringa ISO 8601, e htmlContent è una stringa vuota su una voce mai passata dal redattore. tags è un array dei nomi delle tue aree di prodotto ed è vuoto se non ne è stata assegnata nessuna, learnMoreUrl è null a meno che qualcuno non l'abbia aggiunto in revisione, e il colore di ogni etichetta viene dalla mappa tagColors della risposta, non dalla voce, quindi un'etichetta che hai da allora rimosso dal tuo vocabolario viene semplicemente renderizzata senza colore. nextCursor è null quando hai raggiunto la fine.

Un id feed sconosciuto risponde 404 con {"error":"not_found"}, e lo stesso vale per uno malformato. I due casi sono deliberatamente indistinguibili, quindi questo endpoint non serve a scoprire quali id esistono.

Il feed della roadmap

GET/v1/public/YOUR_PUBLIC_ID/roadmap.json

Le stesse tre colonne che il tuo team mantiene a mano.

{
  "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 è un array, non un oggetto indicizzato per nome di colonna, e il suo ordine fa parte del contratto: planned, poi building, poi shipped. Tutte e tre sono sempre presenti, incluse quelle vuote, così non devi mai distinguere «questa colonna non esiste» da «non ha ancora nulla». Renderizzale nell'ordine ricevuto e corrisponderai a ogni altra superficie che costruiamo.

Un item ha esattamente cinque chiavi: id, column, publicTitle, publicDescription e publishedAt. publicDescription è sempre una stringa e può essere vuota, mai null. Nulla riguardo alla issue da cui proviene un item viene esposto qui, né il repository né il numero della issue, ed è intenzionale, non una svista che colmeremo più avanti.

Questo endpoint non accetta alcun parametro di query. Non c'è cursore, né limit né filtro per repository, perché una roadmap è una bacheca piccola curata da una persona, non un log che cresce all'infinito. Ogni colonna restituisce fino a 50 item e imposta hasMore se ce n'erano di più. hasMore è informativo: non c'è un cursore da seguire, quindi non costruirci intorno un paginatore.

publicTitle e publicDescription sono testo semplice redatto da titoli e corpi di issue, che su un repository pubblico può influenzare chiunque apra una issue. Non hanno alcuna garanzia di sanificazione HTML e non sono l'eccezione di htmlContent. Renderizzali come testo.

Il widget incorporabile

Se preferisci non costruire nulla, aggiungi queste due righe. Il widget è un custom element che renderizza in uno shadow root, così non eredita i tuoi stili né vi si infiltra.

<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>

Entrambi gli attributi sono obbligatori. data-public-id è il tuo id feed, data-api è l'origine da cui il widget carica. Se manca uno dei due, l'elemento scrive un errore in console e non renderizza nulla, che è la prima cosa da controllare se vedi uno spazio vuoto dove dovrebbe stare.

Aggiungi data-theme="dark" all'elemento per una resa scura; la tua pagina può cambiarla a runtime. Per uno stile più profondo, il widget espone proprietà CSS personalizzate (--changelogapp-text, --changelogapp-bg, --changelogapp-accent e altre) e nomi ::part(), che imposti nel tuo foglio di stile. L'app mostra l'anteprima dal vivo di entrambi i temi in Impostazioni, Feed pubblico.

Aggiungi data-repos per mostrare solo alcuni dei tuoi repository, per esempio il changelog di un prodotto sul sito di quel prodotto quando più prodotti condividono un account. Il valore è un elenco di nomi completi owner/repo separati da virgole; un nome senza proprietario non corrisponde a nulla e mostra un feed vuoto senza errori. Vengono considerati al massimo dieci repository. Un widget così limitato mostra solo Updates e Feedback, perché la roadmap non ha una vista per repository, e il feedback viene comunque archiviato dove punta la destinazione feedback del tuo team. In Impostazioni, Feed pubblico c'è un selettore che scrive l'attributo per te.

Renderizza tre schede in quest'ordine: Novità, Roadmap e Feedback. Le prime due leggono i feed di sopra. La terza invia all'endpoint qui sotto e conserva ogni id di invio in localStorage, così chi visita può tornare e vedere cosa è successo a ciò che ha inviato.

Lo script viene servito con versioning. /widget.js serve sempre la build più recente ed è cachato per un'ora, così un rilascio raggiunge i tuoi visitatori senza che tu tocchi nulla. /widget-vN.js fissa una build: una volta servito un numero di versione, i suoi byte non cambiano mai più, ed è cachato per un anno. Fissalo se preferisci adottare i cambiamenti di proposito.

Carica esattamente uno script del widget per pagina

I due URL sono alternative, non livelli. Entrambi registrano lo stesso nome di custom element, e un browser permette di registrare un nome una sola volta per documento: vince lo script che si esegue per primo, per tutta la vita della pagina, e il secondo resta inerte. Quindi una pagina con sia /widget.js sia /widget-v5.js renderizza ciò che il browser ha eseguito per primo per caso, cosa che non controlli; aggiungere /widget-v5.js accanto a un /widget.js esistente per fissare la versione non fa nulla. Di solito vince la build più vecchia, perché è già in cache.

Quando succede, il widget scrive un avviso in console nominando entrambe le build, così non devi indovinare. Non può fare altro che avvisare: quando gira la seconda copia, la prima ha già preso il nome. La soluzione è sempre sostituire il tag script invece di aggiungerne un altro, e lo stesso vale se un tag manager o un parziale te ne inietta uno. Per passare dalla build continua a una fissata, cambia il src.

La pagina di feed ospitata

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

A quello stesso indirizzo ospitiamo anche una pagina semplice: il tuo changelog e la tua bacheca roadmap, renderizzati dagli stessi due feed di sopra. Non richiede accesso né alcuna configurazione da parte tua. È anche dove riportiamo le persone quando un ciclo si chiude: il commento Shipped che lasciamo su una issue GitHub rimanda qui, e lo stesso fa shippedEntry.link dalla ricerca di stato sopra, entrambi arrivando alla voce pubblicata con la propria ancora #entry-ID, che trova la voce anche se nel frattempo è passata a una pagina successiva.

Trattala come un'alternativa, non come l'integrazione. Il feed del changelog e il widget restano il modo per portare questo nel tuo sito così che sembri il tuo prodotto e non il nostro; questa pagina serve per il periodo prima che tu l'abbia fatto, e per i link di chiusura ciclo, che puntano qui a prescindere da cos'altro hai costruito.

Il tuo dominio

Puoi servire la pagina ospitata dal tuo indirizzo, senza modifiche a DNS o certificati. In Impostazioni, Dominio personalizzato, incolla l'indirizzo pubblico che vedranno i tuoi lettori (per esempio https://example.com/changelog), poi fai puntare quel percorso del tuo sito alla destinazione proxy indicata lì: una sola regola copre la pagina, i suoi asset, i suoi dati e i suoi feed. Controlla il mio dominio recupera il tuo indirizzo dal nostro lato e ti dice se il proxy è corretto e, in caso contrario, cosa cambiare.

Il server MCP

POSThttps://api.changeloop.dev/mcp

Se lavori in Claude Code, ChatGPT o un altro agente che parla il Model Context Protocol, puoi collegarlo direttamente al tuo changelog. L'agente può quindi vedere cosa è in attesa di revisione, modificare il testo e pubblicare, senza che tu esca dall'editor. È la stessa porta di revisione della web app: nulla diventa pubblico finché qualcosa non lo approva.

Collegare Claude Code

Crea prima una chiave API (Impostazioni, Chiavi API), poi aggiungi il server con la tua chiave nell'intestazione:

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

Per un client che invece legge una configurazione JSON, la stessa cosa appare così:

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

Non c'è ancora un flusso OAuth. L'autenticazione è la chiave API nell'intestazione, che è esattamente ciò che fanno i due comandi sopra. Revocare quella chiave in Impostazioni disconnette l'agente alla sua richiesta successiva.

Cosa può fare l'agente

Sette strumenti, e l'elenco è deliberatamente corto. Tutto il resto che questo prodotto può fare è raggiungibile tramite l'API REST con la stessa chiave; ogni strumento esposto a un agente è una cosa in più a cui può essere convinto a chiamare.

  • list_pending_entries, list_published_entries, get_entry - leggono le tue voci. Quelle in attesa non sono pubbliche.
  • update_entry - cambia il titolo o il corpo markdown di una voce. L'HTML servito dal feed viene ri-generato dal tuo markdown dal nostro sanitizzatore; un agente non può fornire HTML.
  • approve_entry - pubblica. È pubblico e immediato, e notifica qualsiasi feedback collegato su GitHub. Solo una voce in attesa può essere approvata.
  • discard_entry - tiene una voce fuori dal changelog. Reversibile dalla web app.
  • get_changelog_info - il tuo id feed e gli indirizzi da cui viene servito il tuo changelog.

Cosa non può fare

Ogni strumento è limitato al team a cui appartiene la chiave, e nessuno di essi accetta un team come argomento, quindi non c'è nulla con cui puntare a un altro team anche se qualcuno ci provasse. Il server non accetta una sessione browser, solo una chiave: una richiesta deve allegare la credenziale deliberatamente. E una chiave non può gestire chiavi né scaricare la tua esportazione dati, quindi un agente collegato in questo modo non può crearsi una seconda credenziale né estrarre i tuoi dati in un'unica chiamata.

Chiavi API

Tutto quanto sopra è anonimo e non richiede credenziali. L'API autenticata, le tue impostazioni e la tua casella di revisione, è una superficie diversa, e accetta una sessione browser autenticata o una chiave API. Le chiavi sono per script e agenti: tutto ciò che deve raggiungere il tuo changelog senza una persona davanti alla tastiera.

Authorization: Bearer clapi_YOUR_KEY

Creane una nell'app, in Impostazioni, nella scheda Chiavi API. La chiave viene mostrata una volta, nel momento in cui la crei, e mai più: conserviamo solo un hash di essa, quindi non c'è alcuna schermata che possa mostrartela una seconda volta. Se la perdi, revocala e creane un'altra.

Cosa può fare e non fare una chiave

Una chiave porta lo stesso accesso di un login, limitato all'unico team in cui è stata creata, con due eccezioni deliberate. Non può gestire chiavi API, e non può scaricare la tua esportazione dati. Entrambe richiedono un vero login, così che una chiave trapelata non possa crearsi un ricambio, non possa revocare le chiavi con cui la bloccheresti, e non possa estrarre i dati del tuo team in un'unica richiesta.

Revoca

Una revoca ha effetto alla richiesta successiva. Una chiave revocata risponde 401 esattamente come una sconosciuta, e continua a rispondere 401 anche da un browser che ha ancora una sessione valida, perché una richiesta con intestazione Authorization non viene mai ritentata silenziosamente come richiesta con cookie. La chiave revocata resta elencata con la data di revoca e quella di ultimo utilizzo, che è proprio ciò che serve per capire cosa ha raggiunto una chiave trapelata.

Piani e limiti

Il piano gratuito scrive 20 modifiche unite al mese e limita a 50 al giorno il numero di merge esaminati, feedback smistati, schede di roadmap redatte e versioni alternative; il piano team non ha limiti rigidi. Impostazioni, Piano e utilizzo mostra ogni budget così come il prodotto stesso lo conta, con il momento in cui si azzera, prima che qualcosa venga rifiutato. Il lavoro che arriva oltre un limite viene trattenuto, non perso: una voce oltre quota aspetta nella inbox e una bozza di roadmap rifiutata si può ritentare quando la finestra si rinnova.

GitLab e Bitbucket

Un progetto GitLab o un repository Bitbucket può alimentare il tuo changelog come farebbe un repository GitHub: collegalo in Impostazioni, poi GitLab, o in Impostazioni, poi Bitbucket, aggiungi il webhook che ti diamo (o, su bitbucket.org, lascia che lo aggiunga Connect with Bitbucket, se la pagina Bitbucket offre quel pulsante), e ogni cambiamento unito nel branch che indichi diventa una voce in bozza nella tua casella di revisione, scritta allo stesso modo e sottoposta alla stessa revisione umana. Le voci nascono da pull request o merge request unite oppure, su GitHub e Bitbucket, dai push se scegli la modalità push in Impostazioni, poi What creates drafts. I progetti GitLab generano bozze solo dalle merge request.

Collegare un progetto

I progetti GitLab si collegano in Impostazioni, poi GitLab, e i repository Bitbucket in Impostazioni, poi Bitbucket. Inserisci il percorso (su GitLab gruppo e progetto, come acme/web; su Bitbucket workspace e repository, come acme/app) e ti restituiamo un indirizzo webhook e un segreto. Incolla entrambi nelle impostazioni webhook dall'altra parte: su GitLab seleziona Merge request events, su Bitbucket seleziona i trigger Merged pull request e Push repository. Le istanze autogestite funzionano, via https. Il segreto viene mostrato una volta, in quel momento. Se lo perdi, rimuovi il progetto e ricollegalo. Su bitbucket.org, se la pagina Bitbucket mostra un pulsante Connect with Bitbucket, puoi evitare di incollare: premilo, consenti l'accesso una volta e noi leggiamo il branch principale del repository e aggiungiamo il webhook al posto tuo. Servono i permessi di amministratore sul repository. Con Bitbucket autogestito, o se preferisci incollare, scegli Set it up by hand e ottieni indirizzo e segreto come sopra. Se rimuovi un repository Bitbucket e lo ricolleghi, elimina anche il suo vecchio webhook su Bitbucket, in Repository settings, poi Webhooks. Una volta collegato un progetto, puoi cambiarne il branch e attivare la pubblicazione automatica nella sua riga, e se una consegna è stata ignorata, la riga dice perché.

Perché Bitbucket chiede un branch e GitLab no

GitLab ci dice quale branch il tuo progetto tratta come predefinito, così puoi lasciare il campo vuoto e intendere proprio quello. Bitbucket non invia alcun branch predefinito, quindi se ti lasciassimo il campo vuoto non avremmo nulla con cui confrontarci, e il tuo webhook resterebbe con l'aspetto di essere perfettamente installato senza mai produrre una sola voce. Preferiamo farti una domanda piuttosto che lasciare che succeda. Con Connect with Bitbucket chiediamo il branch principale direttamente a Bitbucket quando consenti l'accesso, quindi non devi digitarlo.

Cosa non coprono ancora

Voci di changelog, e nient'altro. Che il widget di feedback ti apra una issue, che la risposta venga pubblicata su quella issue quando la correzione viene rilasciata, che la roadmap pubblica sia alimentata da etichette delle issue, e l'anteprima della sorgente nella casella di revisione, sono tutte cose oggi esclusive di GitHub.

Preferiamo dire il motivo piuttosto che nasconderlo. Ognuna di queste cose richiede un token di accesso con permesso di scrittura sul tuo progetto, conservato da noi. Le voci di changelog non ne richiedono nessuno, perché tutto ciò da cui vengono scritte arriva nel webhook stesso, quindi collegare GitLab o Bitbucket tramite webhook non ci dà alcuna credenziale né alcun accesso in lettura al tuo codice. Connect with Bitbucket è l'unica eccezione. Bitbucket ci presta, per una sola richiesta, un token che può leggere il repository e le sue pull request e gestirne i webhook, che usiamo solo per leggere il branch principale e aggiungere il webhook, e poi scartiamo. Non viene conservato nulla. Preferiamo consegnare la parte che non ti costa nulla piuttosto che chiedere un token per arrotondare un elenco di funzioni.

Altre versioni di una voce

Un cambiamento va spesso spiegato più di una volta: ai clienti nel changelog, a chi risponde a domande a riguardo, e in un canale dove nessuno legge quattro paragrafi. Dalla casella di revisione puoi redigere due versioni aggiuntive di una voce prima di approvarla.

Una versione di annuncio è di una o due righe, ed è ciò che viene pubblicato su Slack quando approvi la voce, al posto del testo completo. Una nota di supporto è un briefing interno: cosa è cambiato, cosa noteranno i clienti, e una frase che qualcuno del supporto potrebbe dire quasi testualmente. Entrambe sono bozze che puoi riscrivere prima di usarle, ed entrambe si possono rimuovere.

Nessuna delle due viene pubblicata

Queste versioni non appaiono mai sulla tua pagina di changelog, in nessun feed, nel widget o nell'API che li serve. La nota di supporto in particolare è scritta per persone della tua azienda e può essere più diretta della voce stessa. Esiste solo nella tua casella di revisione e, se le usi, nella tua copia personale.

Da cosa vengono scritte

Sempre dalla voce, mai dalla pull request. È intenzionale: la voce è già passata dalla regola che tiene vaghe le correzioni di sicurezza, e dalla tua revisione. Una versione riscritta da essa non può reintrodurre un dettaglio che hai rimosso, perché quel dettaglio non è in ciò che ha ricevuto il modello.

Annunciare su Slack

Approva una voce e può essere pubblicata su un canale Slack nello stesso momento in cui diventa pubblica. Collegalo in Impostazioni, nella scheda Slack: crea un webhook in entrata nel tuo workspace, scegli il canale e incolla l'URL. Oltre a quel webhook non si installa nulla dalla tua parte, e non chiediamo accesso al tuo workspace.

Il messaggio porta il titolo della voce, il testo come lo hai approvato, categoria ed etichette, e un link di ritorno alla voce sul tuo changelog. Il markdown viene tradotto in ciò che Slack renderizza davvero, quindi una voce non arriva mostrando i suoi stessi asterischi.

L'URL del webhook è una credenziale

Chiunque abbia quell'URL può pubblicare nel canale, quindi la trattiamo come una password: viene salvata, e dopo nessuna schermata e nessuna risposta API la mostra mai più, nemmeno la tua stessa esportazione dati. Ciò che vedi dopo è una mascheratura, sufficiente a distinguere due webhook e inutile per chiunque altro. Accettiamo solo un indirizzo hooks.slack.com, quindi un URL scritto male o sostituito viene rifiutato invece che interrogato.

Quando smette di funzionare

Se rimuovi l'app su Slack o archivi il canale, il webhook smette di funzionare in modo permanente. Ce ne accorgiamo al primo messaggio rifiutato, disattiviamo gli annunci e lo indichiamo nella scheda Slack con motivo e data. Che non continuiamo a riprovare in silenzio è intenzionale: un changelog che nessuno ha annunciato appare esattamente come uno che nessuno ha letto, e questa differenza merita di essere comunicata.

Mettere in pausa

Mettere in pausa ferma gli annunci e conserva il webhook, così riprendere è un clic invece di un altro passaggio da Slack. Disconnettere rimuove del tutto l'URL. In entrambi i casi, la pubblicazione stessa non ne risente: Slack è un canale su cui pubblica il tuo changelog, mai una porta che attende. Se Slack non è raggiungibile quando approvi qualcosa, la voce viene comunque pubblicata e l'annuncio viene ritentato da solo.

RSS e JSON Feed

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

Le stesse voci pubblicate come feed sottoscrivibile, nei due formati che i lettori capiscono: RSS 2.0 e JSON Feed 1.1. Entrambi accettano gli stessi filtri repos, category e tag del feed del changelog e hanno lo stesso Cache-Control ed ETag. Nessuno dei due pagina: un lettore interroga la testa del feed, quindi questi restituiscono solo le voci più recenti, senza cursore.

Il testo della voce è l'HTML sanificato, avvolto in CDATA per RSS e come content_html per JSON Feed. JSON Feed porta inoltre i colori delle tue etichette sotto un'estensione con namespace _changelogapp; RSS no, perché nessun lettore li dipingerebbe.

La pagina ospitata li segnala entrambi come link rel="alternate", così un browser o un lettore che vi atterra può iscriversi senza che gli si dicano i percorsi.

Una voce singola

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

Restituisce una singola voce pubblicata, lo stesso oggetto che il feed del changelog porta nel suo array data. È dove puntano i permalink nei feed, ed è utile quando hai un id e non vuoi scorrere il feed per trovarla. Un id sconosciuto, o di una voce non pubblicata, restituisce 404 con lo stesso corpo di qualsiasi altro id sconosciuto.

Il feed markdown

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

Le stesse voci pubblicate come markdown semplice, servite come text/markdown. Esiste per lettori che non sono browser: un LLM o un agente che risponde a cosa è cambiato di recente in questo prodotto riceve il testo senza analizzare RSS o attraversare JSON. Accetta gli stessi filtri repos, category e tag del feed del changelog, ha lo stesso Cache-Control ed ETag, e risponde 304 a una richiesta condizionale esattamente come gli altri due.

Ogni voce è una sezione: il titolo come intestazione, poi una singola riga con data, categoria ed eventuali etichette, poi il testo della voce così com'è stato scritto, poi il link Learn more se la voce ne ha uno, poi il suo permalink. Il documento inizia con titolo e descrizione del tuo feed e rimanda alla pagina ospitata. Se non è ancora stato pubblicato nulla, lo dice in una frase invece di restituire un corpo vuoto, così chi legge può distinguerlo da una richiesta fallita.

La pagina ospitata lo segnala come link rel="alternate" con type text/markdown, accanto ai link RSS e JSON Feed, così un agente che ha caricato l'HTML può trovarlo senza che gli si dica il percorso.

Ciò che viene servito è il markdown che abbiamo redatto e tu hai approvato, non l'HTML sanificato. Questo è sicuro in quanto markdown, che è inerte, ed è per questo che questa risposta non è mai text/html. Se lo renderizzi tu stesso, esegui l'escape come faresti con qualsiasi altro markdown non attendibile: le voci redatte da un repository pubblico possono essere influenzate da chiunque possa aprirvi una pull request.

Raccogliere feedback sul tuo sito

Aggiungi le tue origini prima di testare questo

Questo è l'unico endpoint del prodotto che scrive, quindi non accetta richieste da qualsiasi posto. Confronta l'intestazione Origin del browser con una lista consentita per team, e quella lista parte vuota. Vuota significa rifiutare tutto, non permettere tutto. Finché non aggiungi l'origine su cui incorpori, ogni singolo invio torna con 403 e {"error":"origin_not_allowed"}, e nulla arriva alla tua casella. Se il tuo modulo sembra corretto e fallisce comunque, quasi sempre è questo il motivo. Imposta la lista con un PATCH autenticato su /v1/settings/feed con {"allowedOrigins": ["https://your-site.example"]}, e rileggila con un GET sullo stesso percorso, che risponde con il tuo publicId, i tuoi allowedOrigins, e feedTitle e feedDescription che i tuoi iscritti vedono in un lettore di feed. Salviamo ogni origine esattamente nella forma in cui la invia un browser, quindi una barra finale o una porta predefinita esplicita in ciò che invii non è un problema.

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 deve sembrare un indirizzo email ed essere lungo 254 caratteri o meno. message non può essere vuoto e deve pesare 2 KB o meno, misurati in byte UTF-8, non in caratteri. Il corpo JSON nel suo complesso è limitato a 8 KB. C'è un altro campo, website: è un honeypot, quindi omettilo, o inviarlo vuoto se lo renderizzi come campo nascosto, come fa il nostro widget.

Vale la pena capire l'honeypot prima di debuggare qualsiasi cosa al riguardo. Se website arriva con del contenuto, rispondiamo 202 con un id di invio dall'aspetto perfettamente normale e poi non facciamo nulla, perché un bot che impara di essere stato scoperto semplicemente riprova in modo diverso. È la risposta giusta per un bot e confusa per te, quindi se il tuo modulo ha un campo chiamato website che un browser potrebbe autocompilare, rinominalo o omettilo. Un invio che sembra accettato e non appare mai è quasi sempre questo.

Un invio che accettiamo restituisce 202 con un publicSubmissionId. Restituiscilo a chi lo ha inviato e conservalo se puoi: è l'unico modo per verificare cosa ne è stato dopo.

I casi di errore sono 400 con invalid_email o invalid_message per forma sbagliata, 413 con email_too_large o message_too_large per forma corretta ma troppo grande, 429 con rate_limited oltre 5 invii al minuto o 30 all'ora da un indirizzo contro un feed, 403 con origin_not_allowed, e 404 con not_found per un id feed che non riconosciamo.

C'è anche un tetto giornaliero per team su quanto lavoro a valle possono innescare gli invii. Oltre quello continuiamo comunque ad accettare e salvare tutto, semplicemente aspetta che qualcuno del tuo team lo guardi invece di aprire qualcosa da solo.

Controllare un invio

GET/v1/public/YOUR_PUBLIC_ID/feedback/PUBLIC_SUBMISSION_ID

Risponde con status, più githubIssueUrl non appena esiste una issue per quell'invio, più shippedEntry con titolo e link non appena il lavoro è uscito. L'indirizzo email di chi ha inviato non viene mai letto dal nostro database per questa rotta, tanto meno restituito, ed è ciò che rende la risposta sicura da renderizzare su una pagina che chiunque può vedere. L'id è l'intera credenziale, trattalo come tale. È limitato a 20 richieste al minuto e 200 all'ora per indirizzo e feed.

Cache, CORS e richieste condizionali

Entrambi i feed inviano Cache-Control: public, max-age=60, stale-while-revalidate=300 insieme a un ETag forte. Restituisci quell'ETag come If-None-Match e un feed invariato risponde 304 senza corpo. Nessun campo della risposta porta un valore di orologio, quindi l'ETag resta stabile quando ri-renderizziamo dati invariati, ed è questo che rende affidabili quei 304.

I due feed e la ricerca di stato sono letture anonime e rispondono con Access-Control-Allow-Origin: *, quindi puoi chiamarli da qualsiasi origine, con curl o da uno step di build. Il POST di feedback è l'eccezione: risponde con la tua origine consentita e un Vary: Origin, mai con un jolly. I browser gli fanno preflight, e un preflight risponde sempre 204 sia che l'origine sia consentita sia che non lo sia, quindi non può essere usato per sondare le tue impostazioni.