Documentație pentru dezvoltatori
Ultima actualizare 26 septembrie 2026.
Tot ce publică Changeloop pentru tine este JSON simplu prin HTTPS. Nu există niciun SDK de instalat, nicio cheie API de rotit și niciun pas de autentificare: cele două feeduri de mai jos sunt citiri publice anonime cheiate după ID-ul feedului tău. Înlocuiește YOUR_PUBLIC_ID cu al tău în orice exemplu de pe această pagină.
Un lucru de știut înainte de a începe: ID-ul public al feedului tău se află chiar în aplicație. Autentifică-te, deschide Setări, și este chiar acolo în secțiunea Feed public, cea în care ajungi implicit, împreună cu linkuri gata făcute către changelog.json și roadmap.json, un link către pagina ta de feed găzduită și fragmentul de cod al widgetului de mai jos, fiecare cu propriul buton de copiere.
Primii pași
Cinci pași te duc de la înregistrare la un changelog pe site-ul tău. Pagina „Get started” din aplicație te ghidează prin ei și bifează fiecare pas imediat ce l-ai terminat.
- Conectează o sursă: un repozitoriu GitHub, un proiect GitLab sau un repozitoriu Bitbucket.
- Alege limba în care sunt scrise intrările tale.
- Opțional, creează etichete ca cititorii să poată filtra după zona de produs.
- Publică prima ta intrare. Modificările îmbinate ajung ca ciorne în caseta de recenzie: aprobă una sau activează publicarea automată pentru acel repozitoriu.
- Pune-l pe site-ul tău: adaugă un link către pagina găzduită, lipește widgetul sau afișează feedul JSON în propria pagină.
Changelog-ul tău în aproximativ zece linii de React
Lipește asta într-o componentă și ai un changelog funcțional. Nu mai trebuie adăugat nimic altceva.
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 este markdown-ul pe care l-am redactat, ca text. Dacă preferi să randezi rezultat formatat, folosește în schimb htmlContent: este construit pe server de propriul nostru sanitizator dintr-o listă fixă de taguri și atribute permise, și este singura valoare din oricare dintre aceste răspunsuri menită să fie injectată ca markup. Tot restul este text, iar intrările redactate dintr-un repozitoriu public pot fi influențate de oricine poate deschide acolo un pull request, deci tratează-le corespunzător.
Feedul de changelog
GET/v1/public/YOUR_PUBLIC_ID/changelog.jsonIntrările tale publicate, de la cea mai nouă, cu ID-ul cel mai nou rezolvând egalitățile la marcaje de timp identice.
Parametri de interogare
- repos acceptă o listă separată prin virgule de nume complete de repozitorii, de exemplu acme/web,acme/api. Revin doar intrările din acele repozitorii. Lasă-l gol și le primești pe toate.
- limit este câte intrări vrei per pagină. Implicit este 20, orice peste 50 este limitat la 50, iar orice nu putem interpreta ca un număr pozitiv revine la 20 în loc să eșueze.
- cursor este opac. Ia valoarea nextCursor din răspunsul anterior și returnează-o exact așa. Un cursor pe care nu îl putem decoda este tratat ca și cum nu ar exista niciun cursor, deci primești prima pagină din nou în loc de o eroare.
Răspuns
{
"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" }
}
Fiecare intrare poartă aceleași nouă câmpuri: id, title, mdContent, htmlContent, repoFullName, category, tags, learnMoreUrl și publishedAt. category este feature, fix sau internal și este null dacă cel care a redactat nu a stabilit-o, publishedAt este un șir ISO 8601, iar htmlContent este un șir gol pentru o intrare care nu a trecut niciodată prin redactor. tags este un array cu numele propriilor tale zone de produs și este gol când niciuna nu a fost atribuită, learnMoreUrl este null decât dacă un recenzent l-a adăugat, iar culoarea în care se desenează fiecare tag vine din harta tagColors din răspuns, nu din intrare, deci un tag pe care l-ai eliminat din vocabularul tău se randează pur și simplu fără culoare. nextCursor este null când ajungi la capăt.
Un ID de feed necunoscut răspunde 404 cu {"error":"not_found"}, la fel și unul malformat. Cele două sunt intenționat indistincte, deci acest endpoint nu poate fi folosit pentru a afla care ID-uri există.
Feedul de roadmap
GET/v1/public/YOUR_PUBLIC_ID/roadmap.jsonAceleași trei coloane pe care echipa ta le păstrează deja manual.
{
"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 este un array, nu un obiect indexat după numele coloanei, iar ordinea lui face parte din contract: planned, apoi building, apoi shipped. Toate trei sunt mereu prezente, inclusiv cele goale, deci nu trebuie niciodată să distingi „nu există o astfel de coloană” de „nu are încă nimic în ea”. Randează-le în ordinea în care le-ai primit și te vei potrivi cu orice altă suprafață pe care o construim.
Un element are exact cinci câmpuri: id, column, publicTitle, publicDescription și publishedAt. publicDescription este mereu un șir și poate fi gol, niciodată null. Nimic despre issue-ul din care provine elementul nu este expus aici, nici repozitoriul, nici numărul issue-ului, iar asta este intenționat, nu o omisiune pe care o vom completa mai târziu.
Acest endpoint nu acceptă niciun parametru de interogare. Nu există cursor, limită sau filtru de repozitoriu, deoarece un roadmap este o tablă mică pe care o curatoriază o persoană, nu un jurnal care crește la nesfârșit. Fiecare coloană returnează până la 50 de elemente și setează hasMore dacă a avut mai multe. hasMore este informativ: nu există un cursor pentru a-l urma, deci nu construi un paginator în jurul lui.
publicTitle și publicDescription sunt text simplu redactat din titlurile și corpurile issue-urilor, care pe un repozitoriu public pot fi influențate de oricine deschide un issue. Nu poartă nicio garanție de sanitizare HTML și nu sunt excepția htmlContent. Randează-le ca text.
Widgetul încorporabil
Dacă preferi să nu construiești nimic, adaugă aceste două linii. Widgetul este un custom element care se randează într-un shadow root, deci nu îți moștenește stilurile și nici nu se scurge în ele.
<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>
Ambele atribute sunt obligatorii. data-public-id este ID-ul feedului tău, data-api este originea de la care widgetul preia date. Dacă lipsește oricare, elementul scrie o eroare în consolă și nu randează nimic, ceea ce este primul lucru de verificat dacă vezi un spațiu gol acolo unde ar trebui să fie.
Adaugă data-theme="dark" pe element pentru o randare întunecată; pagina ta o poate comuta în timpul rulării. Pentru o stilizare mai fină, widgetul expune proprietăți CSS personalizate (--changelogapp-text, --changelogapp-bg, --changelogapp-accent și altele) și nume ::part(), pe care le setezi în propria foaie de stil. Aplicația previzualizează ambele teme în timp real în Setări, Feed public.
Adaugă data-repos ca să afișezi doar o parte dintre repository-uri, de exemplu changelog-ul unui produs pe site-ul acelui produs atunci când mai multe produse împart un singur cont. Valoarea este o listă de nume complete owner/repo separate prin virgulă; un nume fără proprietar nu se potrivește cu nimic și afișează un flux gol, fără eroare. Sunt luate în calcul cel mult zece repository-uri. Un widget restrâns astfel afișează doar Updates și Feedback, pentru că roadmap-ul nu are o vedere per repository, iar feedback-ul se înregistrează în continuare acolo unde indică ținta de feedback a echipei tale. În Setări, Flux public există un selector care scrie atributul pentru tine.
Randează trei file în această ordine: Updates, Roadmap și Feedback. Primele două citesc feedurile de mai sus. A treia trimite către endpointul de mai jos și păstrează ID-ul fiecărei trimiteri în localStorage, deci un vizitator se poate întoarce și vedea ce s-a întâmplat cu ce a trimis.
Scriptul este servit versionat. /widget.js servește mereu cel mai nou build și este pus în cache pentru o oră, deci o lansare ajunge la vizitatorii tăi fără să atingi nimic. /widget-vN.js fixează un build: odată ce un număr de versiune a fost servit, octeții lui nu se mai schimbă niciodată, și este pus în cache pentru un an. Fixează-l dacă preferi să adopți modificările intenționat.
Încarcă exact un script de widget per pagină
Cele două URL-uri sunt alternative, nu straturi. Ambele înregistrează același nume de custom element, iar un browser permite ca un nume să fie înregistrat o singură dată per document: oricare script se execută primul câștigă, pe durata de viață a paginii, iar al doilea devine inert. Deci o pagină care conține atât /widget.js cât și /widget-v5.js randează pe oricare l-a executat browser-ul primul, ceea ce nu este ceva ce controlezi, iar adăugarea /widget-v5.js lângă un /widget.js existent pentru a fixa versiunea nu face nimic.
Când se întâmplă asta, widgetul scrie un avertisment în consolă care numește ambele build-uri, deci nu rămâi să ghicești. Nu poate face mai mult decât să avertizeze: până când a doua copie se execută, prima a revendicat deja numele. Soluția este mereu să înlocuiești tagul de script în loc să adaugi altul, și la fel se aplică dacă un manager de taguri sau un fragment îl inserează pentru tine. Pentru a trece de la build-ul continuu la unul fixat, schimbă src-ul.
Pagina de feed găzduită
https://feed.changeloop.dev/feed/YOUR_PUBLIC_IDGăzduim și noi o pagină simplă la acea adresă: changelog-ul și tabla ta de roadmap, randate din aceleași două feeduri de mai sus. Nu necesită autentificare și nici ceva configurat de partea ta. Este de asemenea locul unde trimitem oamenii înapoi odată ce o buclă se închide: comentariul Shipped pe care îl lăsăm pe un issue GitHub duce aici, la fel și shippedEntry.link din căutarea de trimitere de mai sus, ambele ajungând la intrarea livrată cu propria sa ancoră #entry-ID, care încă găsește intrarea chiar dacă între timp s-a mutat pe o pagină ulterioară.
Tratează-o ca o rezervă, nu ca integrarea. Feedul de changelog și widgetul rămân modul de a pune asta pe propriul tău site ca să arate ca produsul tău, nu al nostru; această pagină este pentru când nu ai făcut încă asta, și pentru linkurile de închidere a buclei, care indică aici indiferent de ce altceva ai construit.
Domeniul tău
Poți servi pagina găzduită de la propria adresă, fără modificări de DNS sau certificat. În Setări, Domeniu propriu, lipește adresa publică pe care o vor vedea cititorii (de exemplu https://example.com/changelog), apoi îndreaptă acea cale de pe site-ul tău către ținta de proxy afișată acolo: o singură regulă acoperă pagina, resursele, datele și feedurile ei. Verifică domeniul meu preia adresa ta dinspre noi și îți spune dacă proxy-ul este corect și, dacă nu, ce trebuie schimbat.
Serverul MCP
POSThttps://api.changeloop.dev/mcpDacă lucrezi în Claude Code, ChatGPT sau alt agent care vorbește Model Context Protocol, îl poți conecta direct la changelog-ul tău. Agentul poate atunci vedea ce așteaptă recenzia, poate edita textul și poate publica, fără să părăsești editorul. Este aceeași poartă de recenzie ca aplicația web: nimic nu devine public până când ceva nu aprobă.
Conectarea Claude Code
Creează mai întâi o cheie API (Setări, Chei API), apoi adaugă serverul cu cheia ta în header:
claude mcp add --transport http changeloop \
https://api.changeloop.dev/mcp \
--header "Authorization: Bearer clapi_YOUR_KEY"
Pentru un client care citește o configurație JSON în schimb, același lucru arată așa:
{
"mcpServers": {
"changeloop": {
"type": "http",
"url": "https://api.changeloop.dev/mcp",
"headers": { "Authorization": "Bearer clapi_YOUR_KEY" }
}
}
}
Nu există încă un flux OAuth. Autentificarea este cheia API din header, ceea ce fac cele două comenzi de mai sus. Revocarea acelei chei din Setări deconectează agentul la următoarea sa cerere.
Ce poate face agentul
Șapte instrumente, iar lista este intenționat scurtă. Orice altceva poate face acest produs este accesibil prin API-ul REST cu aceeași cheie; fiecare instrument expus unui agent este încă un lucru la care poate fi convins să apeleze.
- list_pending_entries, list_published_entries, get_entry - citesc intrările tale. Cele în așteptare nu sunt publice.
- update_entry - schimbă titlul sau corpul markdown al unei intrări. HTML-ul pe care îl servește feedul este randat din nou din markdown-ul tău de sanitizatorul nostru; un agent nu poate furniza HTML.
- approve_entry - publică. Aceasta este publică și imediată, și notifică orice feedback conectat de pe GitHub. Doar o intrare în așteptare poate fi aprobată.
- discard_entry - păstrează o intrare în afara changelog-ului. Reversibil din aplicația web.
- get_changelog_info - ID-ul feedului tău și adresele de unde este servit changelog-ul tău.
Ce nu poate face
Fiecare instrument este limitat la echipa căreia îi aparține cheia, și niciunul nu acceptă o echipă ca argument, deci nu există nimic care să indice o altă echipă chiar dacă ceva ar încerca. Serverul nu acceptă o sesiune de browser, doar o cheie: o cerere trebuie să atașeze credențiala intenționat. Iar o cheie nu poate gestiona chei sau descărca exportul datelor tale, deci un agent conectat în acest fel nu își poate crea singur o a doua credențială sau extrage datele tale într-un singur apel.
Chei API
Tot ce e mai sus este anonim și nu necesită nicio credențială. API-ul autentificat - setările tale, caseta ta de recenzie - este o suprafață diferită, și acceptă fie o sesiune de browser autentificată, fie o cheie API. Cheile sunt pentru scripturi și agenți: orice trebuie să ajungă la changelog-ul tău fără o persoană la tastatură.
Authorization: Bearer clapi_YOUR_KEYCreează una în aplicație sub Setări, în fila Chei API. Cheia este afișată o dată, în momentul în care o creezi, și niciodată din nou: stocăm doar un hash al ei, deci nu există niciun ecran nicăieri care să ți-o poată arăta a doua oară. Dacă o pierzi, revoc-o și creează alta.
Ce poate și ce nu poate face o cheie
O cheie poartă același acces ca autentificarea, limitat la singura echipă în care a fost creată, cu două excepții intenționate. Nu poate gestiona chei API și nu poate descărca exportul datelor tale. Ambele necesită o autentificare reală, astfel încât o cheie care se scurge să nu-și poată crea singură înlocuitori, să nu poată revoca cheile pe care le-ai folosi pentru a o bloca, și să nu poată extrage datele echipei tale într-o singură cerere.
Revocarea
Revocarea intră în vigoare la următoarea cerere. O cheie revocată răspunde 401 exact ca una necunoscută, și continuă să răspundă 401 chiar și dintr-un browser care încă deține o sesiune validă, deoarece o cerere care poartă un header Authorization nu este niciodată reîncercată în tăcere ca o cerere de cookie. Cheia revocată rămâne listată cu data la care a fost revocată și data la care a fost folosită ultima dată, ceea ce este ceea ce îți dorești când stabilești unde a ajuns o cheie scursă.
Planuri și limite
Planul gratuit redactează 20 de modificări îmbinate pe lună și plafonează la câte 50 pe zi numărul de îmbinări analizate, de feedbackuri triate, de carduri de roadmap redactate și de versiuni alternative; planul de echipă nu are plafoane fixe. Setări, Plan și utilizare arată fiecare buget exact așa cum îl numără produsul, cu momentul în care se resetează, înainte ca ceva să fie refuzat. Munca sosită peste un plafon este reținută, nu pierdută: o intrare peste cotă așteaptă în inbox, iar o ciornă de roadmap refuzată poate fi reîncercată după ce fereastra se reînnoiește.
GitLab și Bitbucket
Un proiect GitLab sau un repozitoriu Bitbucket poate alimenta changelog-ul tău la fel cum o face un repozitoriu GitHub: conectează-l sub Setări, apoi GitLab, sau Setări, apoi Bitbucket, adaugă webhook-ul pe care ți-l dăm (sau, pe bitbucket.org, lasă Connect with Bitbucket să-l adauge dacă pagina Bitbucket oferă acest buton), și fiecare modificare îmbinată în branch-ul pe care îl numești devine o intrare ciornă în caseta ta de recenzie, scrisă în același mod și supusă aceleiași recenzii umane. Intrările provin din pull request-uri sau merge request-uri îmbinate ori, pe GitHub și Bitbucket, din push-uri dacă alegi modul push sub Setări, apoi What creates drafts. Proiectele GitLab generează ciorne doar din merge request-uri.
Conectarea unui proiect
Proiectele GitLab se conectează sub Setări, apoi GitLab, iar repozitoriile Bitbucket sub Setări, apoi Bitbucket. Introdu calea (pe GitLab grupul și proiectul, ca acme/web, pe Bitbucket workspace-ul și repozitoriul, ca acme/app) și îți returnăm o adresă de webhook și un secret. Lipește ambele în setările de webhook de partea lor: pe GitLab bifează Merge request events, pe Bitbucket bifează declanșatoarele Merged pull request și Push repository. Instanțele auto-găzduite funcționează, prin https. Secretul este afișat o dată, în acel moment. Dacă îl pierzi, elimină proiectul și conectează-l din nou. Pe bitbucket.org, dacă pagina Bitbucket afișează un buton Connect with Bitbucket, poți sări peste lipire: apasă-l, permite accesul o singură dată, iar noi citim branch-ul principal al repository-ului și adăugăm webhook-ul pentru tine. Ai nevoie de drepturi de administrator pe repository. Pentru Bitbucket auto-găzduit, sau dacă preferi să lipești, alege Set it up by hand și primești adresa și secretul ca mai sus. Dacă elimini un repozitoriu Bitbucket și îl conectezi din nou, șterge și webhook-ul vechi de pe Bitbucket, în Repository settings, apoi Webhooks. După ce un proiect este conectat, îi poți schimba branch-ul și poți activa publicarea automată pe rândul lui, iar dacă o livrare a fost ignorată, rândul spune de ce.
De ce Bitbucket cere un branch și GitLab nu
GitLab ne spune ce branch tratează proiectul tău ca implicit, deci poți lăsa câmpul gol și asta să însemne acel lucru. Bitbucket nu trimite deloc un branch implicit, deci dacă te-am lăsa să-l lași gol, nu am avea cu ce să comparăm, iar webhook-ul tău ar sta acolo părând instalat perfect fără să producă vreodată o singură intrare. Preferăm să punem o întrebare decât să lăsăm asta să se întâmple. Cu Connect with Bitbucket îi cerem lui Bitbucket branch-ul principal când permiți accesul, deci nu trebuie să-l tastezi.
Ce nu acoperă încă
Intrările de changelog, și nimic altceva. Widgetul de feedback care deschide un issue pentru tine, răspunsul postat înapoi pe acel issue când remedierea este livrată, roadmap-ul public condus de etichetele issue-urilor și previzualizarea sursei din caseta de recenzie sunt toate exclusiv pentru GitHub astăzi.
Motivul este unul pe care preferăm să-l declarăm decât să-l ascundem. Fiecare dintre acestea are nevoie de un token de acces cu permisiune de scriere la proiectul tău, păstrat de noi. Intrările de changelog nu au nevoie de niciunul, deoarece tot ce le scrie sosește chiar în webhook, deci conectarea GitLab sau Bitbucket prin webhook nu ne oferă nicio credențială și nicio citire a codului tău. Connect with Bitbucket este singura excepție. Bitbucket ne împrumută, pentru o singură cerere, un token care poate citi repository-ul și pull request-urile lui și îi poate gestiona webhook-urile, pe care îl folosim doar pentru a citi branch-ul principal și a adăuga webhook-ul, apoi îl aruncăm. Nu se păstrează nimic. Preferăm să livrăm partea care nu te costă nimic decât să cerem un token pentru a completa o listă de funcții.
Alte versiuni ale unei intrări
O modificare trebuie de obicei explicată de mai multe ori: clienților în changelog, oricui răspunde la întrebări despre ea, și pe un canal unde nimeni nu citește patru paragrafe. Din caseta de recenzie poți redacta una dintre cele două versiuni suplimentare ale unei intrări înainte de a o aproba.
O versiune de anunț are una sau două linii, și asta se postează pe Slack când aprobi intrarea, în locul textului complet. O notă de suport este un briefing intern: ce s-a schimbat, ce vor observa clienții, și o propoziție pe care un agent ar putea-o spune aproape identic. Ambele sunt ciorne pe care le poți rescrie înainte de a fi folosite, și oricare poate fi eliminată.
Niciuna nu este publicată
Aceste versiuni nu apar niciodată pe pagina ta de changelog, în niciun feed, în widget sau prin API-ul care le servește. Nota de suport în special este scrisă pentru oameni din interiorul companiei tale și poate fi mai directă decât intrarea în sine. Singurele locuri unde există sunt caseta ta de recenzie și, dacă le folosești, propria ta copie.
Din ce sunt scrise
Mereu din intrare, niciodată din pull request. Asta este intenționat: intrarea a trecut deja prin regula care păstrează remedierile de securitate vagi, și prin propria ta recenzie. O versiune rescrisă din ea nu poate reintroduce un detaliu pe care l-ai eliminat, deoarece acel detaliu nu se află în ce a primit modelul.
Anunțarea pe Slack
Aprobă o intrare și poate fi postată pe un canal Slack chiar în momentul în care devine publică. Conectează-l sub Setări, în fila Slack: creează un webhook de intrare în propriul tău workspace, alege canalul și lipește URL-ul. Nimic nu este instalat de partea ta în afară de acel webhook, și nu cerem niciun acces la workspace-ul tău.
Mesajul poartă titlul intrării, textul așa cum l-ai aprobat, categoria și etichetele ei, și un link înapoi la intrarea din changelog-ul tău. Markdown-ul este tradus în ce randează efectiv Slack, deci o intrare nu sosește arătându-și propriile asteriscuri.
URL-ul webhook-ului este o credențială
Oricine deține acel URL poate posta pe canal, deci îl tratăm ca o parolă: este stocat, iar după aceea niciun ecran și niciun răspuns API nu îl mai arată din nou, inclusiv propriul tău export de date. Ce vezi după este o mască, suficientă pentru a distinge două webhook-uri și inutilă pentru oricine altcineva. Acceptăm doar o adresă hooks.slack.com, deci un URL greșit scris sau înlocuit este respins în loc să fie preluat.
Când încetează să funcționeze
Dacă elimini aplicația pe Slack sau arhivezi canalul, webhook-ul încetează permanent să funcționeze. Observăm asta la primul mesaj respins, oprim anunțurile, și o menționăm în fila Slack cu motivul și data. Este intenționat că nu continuăm să reîncercăm în tăcere: un changelog pe care nimeni nu l-a anunțat arată exact ca unul pe care nimeni nu l-a citit, iar asta este o diferență care merită comunicată.
Punerea pe pauză
Pauza oprește anunțurile și păstrează webhook-ul, deci reluarea este o singură apăsare în loc de un alt tur prin Slack. Deconectarea elimină URL-ul complet. Oricum ar fi, publicarea în sine nu este afectată: Slack este un canal pe care changelog-ul tău postează, niciodată o poartă pe care o așteaptă. Dacă Slack este inaccesibil când aprobi ceva, intrarea se publică oricum, iar anunțul este reîncercat de la sine.
RSS și JSON Feed
GEThttps://api.changeloop.dev/v1/public/YOUR_PUBLIC_ID/rss.xmlGEThttps://api.changeloop.dev/v1/public/YOUR_PUBLIC_ID/feed.jsonAceleași intrări publicate ca un feed la care se poate abona, în cele două formate pe care le înțeleg cititoarele: RSS 2.0 și JSON Feed 1.1. Ambele acceptă aceleași filtre repos, category și tag ca feedul de changelog și poartă același Cache-Control și ETag. Niciunul nu paginare: un cititor interoghează capătul feedului, deci acestea returnează doar cele mai recente intrări, fără cursor.
Textul intrării este HTML-ul sanitizat, învelit în CDATA pentru RSS și ca content_html pentru JSON Feed. JSON Feed poartă în plus culorile tagurilor tale sub o extensie cu namespace _changelogapp; RSS nu, deoarece niciun cititor nu le-ar colora.
Pagina găzduită anunță ambele ca linkuri rel="alternate", deci un browser sau cititor care ajunge acolo se poate abona fără să i se spună căile.
O intrare singură
GEThttps://api.changeloop.dev/v1/public/YOUR_PUBLIC_ID/entries/ENTRY_IDReturnează o singură intrare publicată, același obiect pe care feedul de changelog îl poartă în array-ul său data. Aici indică linkurile permanente din feeduri, și este util când ai un ID și nu vrei să paginezi feedul pentru a-l găsi. Un ID necunoscut, sau care aparține unei intrări nepublicate, returnează 404 cu același corp ca orice alt ID necunoscut.
Feedul markdown
GEThttps://api.changeloop.dev/v1/public/YOUR_PUBLIC_ID/changelog.mdAceleași intrări publicate ca markdown simplu, servit ca text/markdown. Există pentru cititori care nu sunt browsere: un LLM sau un agent care răspunde la „ce s-a schimbat recent în acest produs” primește textul fără să analizeze RSS sau să parcurgă JSON. Acceptă aceleași filtre repos, category și tag ca feedul de changelog, poartă același Cache-Control și ETag, și răspunde 304 la o cerere condiționată exact ca celelalte două.
Fiecare intrare este o secțiune: titlul ca antet, apoi o singură linie care poartă data, categoria și orice etichete, apoi textul intrării așa cum a fost scris, apoi linkul Learn more dacă intrarea îl are, apoi linkul său permanent. Documentul se deschide cu titlul și descrierea feedului tău și duce înapoi la pagina găzduită. Când nimic nu este încă publicat, spune asta într-o propoziție în loc să returneze un corp gol, deci un cititor poate distinge asta de o preluare eșuată.
Pagina găzduită îl anunță ca un link rel="alternate" cu type text/markdown, alături de linkurile RSS și JSON Feed, deci un agent care a preluat HTML-ul îl poate găsi fără să i se spună calea.
Ce servește este markdown-ul pe care l-am redactat și tu l-ai aprobat, nu HTML-ul sanitizat. Asta este sigur ca markdown, care este inert, și de aceea acest răspuns nu este niciodată text/html. Dacă îl randezi singur, fă-i escape la fel cum ai face escape la orice alt markdown nesigur: intrările redactate dintr-un repozitoriu public pot fi influențate de oricine poate deschide acolo un pull request.
Colectarea feedbackului de pe propriul tău site
Adaugă originile tale înainte de a testa asta
Acesta este singurul endpoint din produs care scrie, deci nu acceptă cereri de oriunde. Se potrivește header-ul Origin al browser-ului cu o listă de permisiuni per echipă, iar acea listă începe goală. Gol înseamnă respinge tot, nu permite tot. Până adaugi originea pe care o încorporezi, fiecare trimitere revine cu 403 și {"error":"origin_not_allowed"}, și nimic nu ajunge în caseta ta de intrare. Dacă formularul tău pare corect și totuși eșuează, aproape mereu asta este cauza. Setează lista cu un PATCH autentificat către /v1/settings/feed care poartă {"allowedOrigins": ["https://your-site.example"]}, și citește-o înapoi cu un GET către aceeași cale, care răspunde cu publicId-ul tău, allowedOrigins-ul tău, și feedTitle și feedDescription pe care abonații tăi le văd într-un cititor de feed. Stocăm fiecare origine exact în forma în care o trimite un browser, deci un slash final sau un port implicit explicit în ce trimiți nu este o problemă.
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 trebuie să arate ca o adresă de e-mail și să aibă 254 de caractere sau mai puțin. message trebuie să fie ne-gol și de 2KB sau mai puțin, măsurat în octeți UTF-8 nu caractere. Corpul JSON în ansamblu este limitat la 8KB. Mai există un câmp, website: este un honeypot, deci omite-l, sau trimite-l gol dacă îl randezi ca o intrare ascunsă așa cum face widgetul nostru.
Merită să înțelegi honeypot-ul înainte de a depana orice cu el. Dacă website sosește cu ceva scris în el, răspundem 202 cu un ID de trimitere perfect normal la aspect și apoi nu facem nimic, deoarece un bot care află că a fost prins pur și simplu încearcă din nou diferit. Acesta este răspunsul corect pentru un bot și unul confuz pentru tine, deci dacă propriul tău formular are un câmp numit website pe care un browser l-ar putea completa automat, redenumește-l sau elimină-l. O trimitere care pare acceptată și nu apare niciodată este aproape mereu asta.
O trimitere pe care o acceptăm returnează 202 cu un publicSubmissionId. Dă-l înapoi persoanei care l-a trimis și păstrează-l dacă poți: este singurul mod în care poate afla ce s-a întâmplat mai departe.
Modurile de eșec sunt 400 cu invalid_email sau invalid_message pentru forma greșită, 413 cu email_too_large sau message_too_large pentru forma corectă dar prea mare, 429 cu rate_limited peste 5 trimiteri pe minut sau 30 pe oră de la o adresă către un feed, 403 cu origin_not_allowed, și 404 cu not_found pentru un ID de feed pe care nu îl recunoaștem.
Există și o limită zilnică per echipă pentru câtă muncă din aval pot declanșa trimiterile. Dincolo de ea, tot acceptăm și stocăm tot ce vine, pur și simplu așteaptă ca cineva din echipa ta să se uite în loc să deschidă ceva singur.
Verificarea unei trimiteri
GET/v1/public/YOUR_PUBLIC_ID/feedback/PUBLIC_SUBMISSION_IDRăspunde cu status, plus githubIssueUrl de îndată ce există un issue pentru acea trimitere, plus shippedEntry care poartă un titlu și un link de îndată ce munca este gata. Adresa de e-mail a expeditorului nu este niciodată citită din baza noastră de date pentru această rută, cu atât mai puțin returnată, ceea ce face răspunsul sigur de randat pe o pagină pe care oricine o poate vedea. ID-ul este întreaga credențială, tratează-l ca atare. Este limitat la 20 de cereri pe minut și 200 pe oră per adresă și feed.
Cache, CORS și cereri condiționate
Ambele feeduri trimit Cache-Control: public, max-age=60, stale-while-revalidate=300 împreună cu un ETag puternic. Trimite acel ETag înapoi ca If-None-Match, iar un feed neschimbat răspunde 304 fără corp. Niciun câmp de răspuns nu poartă o valoare de ceas de perete, deci ETag-ul rămâne stabil când randăm din nou date care nu s-au schimbat, ceea ce face acele 304-uri demne de încredere.
Cele două feeduri și căutarea de trimitere sunt citiri anonime și răspund cu Access-Control-Allow-Origin: *, deci le poți apela din orice origine, din curl, sau dintr-un pas de build. POST-ul de feedback este excepția: răspunde cu propria ta origine permisă și un Vary: Origin, niciodată cu un wildcard. Browserele fac preflight pe el, iar un preflight răspunde mereu 204 indiferent dacă originea este permisă, deci nu poate fi folosit pentru a sonda setările tale.