Pular para o conteúdo

Documentação para desenvolvedores

Última atualização em 26 de setembro de 2026.

Tudo que o Changeloop publica para você é JSON simples sobre HTTPS. Não há SDK para instalar, nenhuma chave de API para rotacionar e nenhuma etapa de login: os dois feeds abaixo são leituras públicas anônimas identificadas pelo ID do seu feed. Substitua YOUR_PUBLIC_ID pelo seu em qualquer exemplo nesta página.

Uma coisa para saber antes de começar: o ID público do seu feed está no próprio app. Faça login, abra Configurações, e ele está bem ali na seção Feed público, aquela em que você cai por padrão, junto com links prontos para changelog.json e roadmap.json, um link para sua página de feed hospedada e o trecho de código do widget abaixo, cada um com seu próprio botão de copiar.

Primeiros passos

Cinco passos levam você do cadastro a um changelog no seu próprio site. A página "Get started" no app guia você por eles e marca cada um assim que é concluído.

  1. Conecte uma fonte: um repositório do GitHub, um projeto do GitLab ou um repositório do Bitbucket.
  2. Escolha o idioma em que suas entradas são escritas.
  3. Se quiser, crie tags para que os leitores possam filtrar por área do produto.
  4. Publique sua primeira entrada. As mudanças mescladas chegam como rascunhos na caixa de revisão: aprove uma ou ative a publicação automática para esse repositório.
  5. Coloque-o no seu site: crie um link para a sua página hospedada, cole o widget ou exiba o feed JSON na sua própria página.

Seu changelog em cerca de dez linhas de React

Cole isso em um componente e você tem um changelog funcionando. Não há mais nada a adicionar.

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 é o markdown que redigimos, como texto. Se preferir renderizar saída formatada, use htmlContent: ele é construído no servidor pelo nosso próprio sanitizador a partir de uma lista fixa de tags e atributos permitidos, e é o único valor em qualquer uma dessas respostas destinado a ser injetado como marcação. Todo o resto é texto, e entradas redigidas a partir de um repositório público podem ser influenciadas por qualquer pessoa que consiga abrir um pull request ali, então trate-as de acordo.

O feed de changelog

GET/v1/public/YOUR_PUBLIC_ID/changelog.json

Suas entradas publicadas, da mais nova para a mais antiga, com o ID mais recente desempatando carimbos de data/hora idênticos.

Parâmetros de consulta

  • repos aceita uma lista separada por vírgulas de nomes completos de repositórios, por exemplo acme/web,acme/api. Só voltam entradas desses repositórios. Deixe em branco e você recebe todas.
  • limit é quantas entradas você quer por página. O padrão é 20, qualquer valor acima de 50 é limitado a 50, e qualquer coisa que não consigamos interpretar como número positivo volta a 20 em vez de falhar.
  • cursor é opaco. Pegue o valor nextCursor da resposta anterior e devolva exatamente como veio. Um cursor que não conseguimos decodificar é tratado como se não houvesse cursor, então você recebe a primeira página novamente em vez de um erro.

Resposta

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

Toda entrada carrega as mesmas nove chaves: id, title, mdContent, htmlContent, repoFullName, category, tags, learnMoreUrl e publishedAt. category é feature, fix ou internal, e é null quando quem redigiu não definiu uma; publishedAt é uma string ISO 8601, e htmlContent é uma string vazia em uma entrada que nunca passou pelo redator. tags é um array com os nomes das suas próprias áreas de produto e fica vazio quando nenhuma foi atribuída; learnMoreUrl é null a menos que um revisor tenha adicionado um, e a cor de cada tag vem do mapa tagColors na resposta, não da entrada, então uma tag que você removeu do seu vocabulário simplesmente renderiza sem cor. nextCursor é null quando você chega ao fim.

Um ID de feed desconhecido responde 404 com {"error":"not_found"}, assim como um malformado. Os dois são deliberadamente indistinguíveis, então este endpoint não pode ser usado para descobrir quais IDs existem.

O feed de roadmap

GET/v1/public/YOUR_PUBLIC_ID/roadmap.json

As mesmas três colunas que sua equipe já mantém manualmente.

{
  "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 é um array, não um objeto indexado por nome de coluna, e sua ordem faz parte do contrato: planned, depois building, depois shipped. As três estão sempre presentes, inclusive as vazias, então você nunca precisa distinguir "essa coluna não existe" de "ainda não tem nada nela". Renderize na ordem em que as recebe e você fica alinhado com toda outra superfície que construímos.

Um item tem exatamente cinco chaves: id, column, publicTitle, publicDescription e publishedAt. publicDescription é sempre uma string e pode estar vazia, nunca null. Nada sobre a issue de onde o item veio é exposto aqui, nem o repositório nem o número da issue, e isso é deliberado, não um descuido que preencheremos depois.

Este endpoint não aceita nenhum parâmetro de consulta. Não há cursor, limite nem filtro de repositório, porque um roadmap é um quadro pequeno que alguém organiza, não um log que cresce para sempre. Cada coluna retorna até 50 itens e define hasMore se havia mais que isso. hasMore é informativo: não há cursor para segui-lo, então não construa uma paginação em torno dele.

publicTitle e publicDescription são texto simples redigido a partir de títulos e corpos de issues, que em um repositório público podem ser influenciados por qualquer pessoa que abra uma issue. Não carregam nenhuma garantia de sanitização HTML e não são a exceção do htmlContent. Renderize-os como texto.

O widget incorporável

Se preferir não construir nada, insira estas duas linhas. O widget é um custom element que renderiza dentro de um shadow root, então nem herda seus estilos nem vaza para eles.

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

Ambos os atributos são obrigatórios. data-public-id é o ID do seu feed, data-api é a origem de onde o widget busca dados. Se algum estiver faltando, o elemento escreve um erro no console e não renderiza nada, o que é a primeira coisa a verificar se você vir um espaço vazio onde deveria estar.

Adicione data-theme="dark" ao elemento para uma renderização escura; sua página pode alternar isso em tempo de execução. Para um estilo mais profundo, o widget expõe propriedades CSS personalizadas (--changelogapp-text, --changelogapp-bg, --changelogapp-accent e outras) e nomes ::part(), que você define na sua própria folha de estilos. O app mostra os dois temas ao vivo em Configurações, Feed público.

Adicione data-repos para mostrar apenas alguns dos seus repositórios, por exemplo o changelog de um produto no site desse produto quando vários produtos compartilham uma conta. O valor é uma lista de nomes completos owner/repo separados por vírgula; um nome sem o dono não corresponde a nada e mostra um feed vazio sem erro. São considerados até dez repositórios. Um widget assim delimitado mostra só Updates e Feedback, porque o roadmap não tem visão por repositório, e o feedback continua sendo registrado onde o destino de feedback da sua equipe aponta. Em Configurações, Feed público há um seletor que escreve o atributo para você.

Ele renderiza três abas nesta ordem: Updates, Roadmap e Feedback. As duas primeiras leem os feeds acima. A terceira envia para o endpoint abaixo e guarda o ID de cada envio no localStorage, então um visitante pode voltar e ver o que aconteceu com o que enviou.

O script é servido versionado. /widget.js sempre serve o build mais recente e é armazenado em cache por uma hora, então um lançamento chega aos seus visitantes sem você tocar em nada. /widget-vN.js fixa um build: assim que um número de versão foi servido, seus bytes nunca mudam de novo, e fica em cache por um ano. Fixe se preferir adotar mudanças de propósito.

Carregue exatamente um script do widget por página

As duas URLs são alternativas, não camadas. Ambas registram o mesmo nome de custom element, e um navegador só permite registrar um nome uma vez por documento: quem executar primeiro vence, pela vida útil da página, e o segundo fica inerte. Então uma página com /widget.js e /widget-v5.js renderiza qualquer um que o navegador tenha executado primeiro, o que não é algo que você controla, e adicionar /widget-v5.js ao lado de um /widget.js existente para fixar a versão não faz nada.

Quando isso acontece, o widget escreve um aviso no console nomeando os dois builds, então você não fica adivinhando. Ele não pode fazer mais que avisar: quando a segunda cópia executa, a primeira já reivindicou o nome. A correção é sempre substituir a tag de script em vez de adicionar outra, e o mesmo vale se um gerenciador de tags ou um parcial inserir uma para você. Para migrar do build contínuo para um fixo, mude o src.

A página de feed hospedada

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

Também hospedamos uma página simples nesse endereço: seu changelog e seu quadro de roadmap, renderizados a partir dos mesmos dois feeds acima. Não exige login nem nada configurado do seu lado. É também para onde enviamos as pessoas quando um loop se fecha: o comentário Shipped que deixamos em uma issue do GitHub aponta para cá, assim como shippedEntry.link da consulta de envio acima, ambos chegando à entrada entregue com sua própria âncora #entry-ID, que ainda encontra a entrada mesmo que ela tenha ido para uma página posterior.

Trate-a como um recurso alternativo, não a integração. O feed de changelog e o widget continuam sendo a forma de colocar isso no seu próprio site para que pareça seu produto em vez do nosso; esta página serve para quando você ainda não fez isso, e para links de fechamento de loop, que apontam para cá independentemente do que mais você tenha construído.

Seu próprio domínio

Você pode servir a página hospedada a partir do seu próprio endereço, sem mudanças de DNS ou certificado. Em Configurações, Domínio próprio, cole o endereço público que seus leitores verão (por exemplo https://example.com/changelog) e aponte esse caminho no seu site para o destino do proxy mostrado ali: uma única regra cobre a página, seus assets, seus dados e seus feeds. Verificar meu domínio busca seu endereço do nosso lado e diz se o proxy está certo e, se não, o que mudar.

O servidor MCP

POSThttps://api.changeloop.dev/mcp

Se você trabalha no Claude Code, ChatGPT ou outro agente que fala Model Context Protocol, pode conectá-lo diretamente ao seu changelog. O agente pode então ver o que está aguardando revisão, editar o texto e publicar, sem você sair do editor. É o mesmo portão de revisão do app web: nada se torna público sem que algo aprove.

Conectando o Claude Code

Primeiro crie uma chave de API (Configurações, Chaves de API), depois adicione o servidor com sua chave no cabeçalho:

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

Para um cliente que lê uma configuração JSON em vez disso, a mesma coisa fica assim:

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

Ainda não há um fluxo OAuth. A autenticação é a chave de API no cabeçalho, que é o que os dois comandos acima fazem. Revogar essa chave em Configurações desconecta o agente na próxima solicitação dele.

O que o agente pode fazer

Sete ferramentas, e a lista é deliberadamente curta. Qualquer outra coisa que este produto possa fazer é alcançável pela API REST com a mesma chave; toda ferramenta exposta a um agente é mais uma coisa que ele pode ser convencido a chamar.

  • list_pending_entries, list_published_entries, get_entry - leem suas entradas. As pendentes não são públicas.
  • update_entry - muda o título ou o corpo em markdown de uma entrada. O HTML que o feed serve é renderizado novamente a partir do seu markdown pelo nosso sanitizador; um agente não pode fornecer HTML.
  • approve_entry - publica. Isso é público e imediato, e notifica qualquer feedback vinculado no GitHub. Só uma entrada pendente pode ser aprovada.
  • discard_entry - mantém uma entrada fora do changelog. Reversível pelo app web.
  • get_changelog_info - o ID do seu feed e os endereços onde seu changelog é servido.

O que não pode fazer

Toda ferramenta é delimitada à equipe a que a chave pertence, e nenhuma delas aceita uma equipe como argumento, então não há nada para apontar a outra equipe mesmo que algo tentasse. O servidor não aceita uma sessão de navegador, apenas uma chave: uma solicitação precisa anexar a credencial deliberadamente. E uma chave não pode gerenciar chaves nem baixar sua exportação de dados, então um agente conectado dessa forma não pode gerar uma segunda credencial para si nem extrair seus dados em uma única chamada.

Chaves de API

Tudo acima é anônimo e não exige credencial. A API autenticada - suas configurações, sua caixa de revisão - é uma superfície diferente, e aceita uma sessão de navegador logada ou uma chave de API. Chaves são para scripts e agentes: qualquer coisa que precise alcançar seu changelog sem uma pessoa no teclado.

Authorization: Bearer clapi_YOUR_KEY

Crie uma no app em Configurações, na aba Chaves de API. A chave é mostrada uma vez, no momento em que você a cria, e nunca mais: armazenamos apenas um hash dela, então não existe nenhuma tela em lugar nenhum que possa mostrá-la a você uma segunda vez. Se perdê-la, revogue e crie outra.

O que uma chave pode e não pode fazer

Uma chave carrega o mesmo acesso que fazer login, delimitado à única equipe em que foi criada, com duas exceções deliberadas. Não pode gerenciar chaves de API, e não pode baixar sua exportação de dados. As duas exigem um login real, para que uma chave vazada não possa gerar substitutas para si mesma, não possa revogar as chaves que você usaria para bloqueá-la, e não possa extrair os dados da sua equipe em uma única solicitação.

Revogando

Revogar entra em vigor na próxima solicitação. Uma chave revogada responde 401 exatamente como uma desconhecida, e continua respondendo 401 mesmo de um navegador que ainda tem uma sessão válida, porque uma solicitação com um cabeçalho Authorization nunca é silenciosamente tentada novamente como uma solicitação de cookie. A chave revogada permanece listada com a data em que foi revogada e a data em que foi usada pela última vez, o que é o que você quer ao descobrir aonde uma chave vazada chegou.

Planos e limites

O plano gratuito redige 20 mudanças mescladas por mês e limita a 50 por dia o número de merges analisados, envios de feedback triados, cards de roadmap redigidos e versões alternativas; o plano de equipe não tem limites rígidos. Configurações, Plano e uso mostra cada orçamento exatamente como o produto o conta, com o momento em que ele reinicia, antes que algo seja recusado. O trabalho que chega acima de um limite fica retido, não se perde: uma entrada acima da cota espera na caixa de entrada, e um rascunho de roadmap recusado pode ser tentado de novo quando a janela virar.

GitLab e Bitbucket

Um projeto GitLab ou um repositório Bitbucket pode alimentar seu changelog da mesma forma que um repositório GitHub: conecte em Configurações, depois GitLab, ou em Configurações, depois Bitbucket, adicione o webhook que fornecemos (ou, no bitbucket.org, deixe o Connect with Bitbucket adicioná-lo se a página do Bitbucket oferecer esse botão), e toda mudança mesclada no branch que você indicar se torna uma entrada de rascunho na sua caixa de revisão, escrita da mesma forma e sujeita à mesma revisão humana. As entradas vêm de pull requests ou merge requests mesclados ou, no GitHub e no Bitbucket, de pushes se você escolher o modo push em Configurações, depois What creates drafts. Projetos do GitLab geram rascunhos apenas a partir de merge requests.

Conectando um projeto

Projetos do GitLab se conectam em Configurações, depois GitLab, e repositórios do Bitbucket em Configurações, depois Bitbucket. Digite o caminho (no GitLab, o grupo e o projeto, como acme/web; no Bitbucket, o workspace e o repositório, como acme/app) e devolvemos um endereço de webhook e um segredo. Cole os dois nas configurações de webhook do lado deles: no GitLab marque Merge request events, no Bitbucket marque os gatilhos Merged pull request e Push repository. Instâncias autogerenciadas funcionam, via https. O segredo é mostrado uma vez, naquele momento. Se perdê-lo, remova o projeto e conecte novamente. No bitbucket.org, se a página do Bitbucket mostrar um botão Connect with Bitbucket, você pode pular a colagem: clique nele, autorize o acesso uma vez e nós lemos o branch principal do repositório e adicionamos o webhook para você. Você precisa de permissão de administrador no repositório. Com o Bitbucket autogerenciado, ou se preferir colar, escolha Set it up by hand e você recebe o endereço e o segredo como acima. Se remover um repositório do Bitbucket e conectá-lo de novo, apague também o webhook antigo no Bitbucket, em Repository settings, depois Webhooks. Depois que um projeto é conectado, você pode mudar o branch dele e ativar a publicação automática na linha do projeto, e se uma entrega foi ignorada, a linha diz o motivo.

Por que o Bitbucket pede um branch e o GitLab não

O GitLab nos diz qual branch seu projeto trata como padrão, então você pode deixar o campo em branco e significar isso. O Bitbucket não envia nenhum branch padrão, então se deixássemos você deixar em branco não teríamos nada para comparar e seu webhook ficaria parecendo perfeitamente instalado sem nunca produzir uma única entrada. Preferimos fazer uma pergunta a deixar isso acontecer. Com o Connect with Bitbucket, perguntamos o branch principal ao Bitbucket quando você autoriza o acesso, então você não precisa digitá-lo.

O que ainda não cobrem

Entradas de changelog, e mais nada. O widget de feedback abrindo uma issue para você, a resposta postada de volta naquela issue quando a correção é entregue, o roadmap público movido por rótulos de issues, e a pré-visualização de código-fonte na caixa de revisão são hoje exclusivos do GitHub.

O motivo é um que preferimos declarar a disfarçar. Cada um desses recursos precisa de um token de acesso com permissão de escrita no seu projeto, mantido por nós. Entradas de changelog não precisam de nenhum, porque tudo a partir do que são escritas chega no próprio webhook, então conectar GitLab ou Bitbucket pelo webhook não nos dá nenhuma credencial nem nenhuma leitura do seu código. O Connect with Bitbucket é a única exceção. O Bitbucket nos empresta, para uma única requisição, um token que pode ler o repositório e seus pull requests e gerenciar seus webhooks, que usamos só para ler o branch principal e adicionar o webhook, e depois descartamos. Nada é guardado. Preferimos entregar a parte que não custa nada a você do que pedir um token só para completar uma lista de recursos.

Outras versões de uma entrada

Uma mudança geralmente precisa ser explicada mais de uma vez: aos clientes no changelog, a quem responde perguntas sobre ela, e em um canal onde ninguém lê quatro parágrafos. Na caixa de revisão, você pode redigir uma de duas versões extras de uma entrada antes de aprová-la.

Uma versão de anúncio tem uma ou duas linhas, e é o que é postado no Slack quando você aprova a entrada, no lugar do texto completo. Uma nota de suporte é um briefing interno: o que mudou, o que os clientes vão notar, e uma frase que um atendente poderia dizer quase literalmente. Ambas são rascunhos que você pode reescrever antes de serem usados, e qualquer uma pode ser removida.

Nenhuma delas é publicada

Essas versões nunca aparecem na sua página de changelog, em nenhum feed, no widget nem na API que os serve. A nota de suporte em particular é escrita para pessoas dentro da sua empresa e pode ser mais direta que a própria entrada. Os únicos lugares onde ela existe são sua caixa de revisão e, se você usar, sua própria cópia dela.

A partir do que são escritas

Sempre a partir da entrada, nunca do pull request. Isso é deliberado: a entrada já passou pela regra que mantém correções de segurança vagas, e pela sua própria revisão. Uma versão reescrita a partir dela não pode reintroduzir um detalhe que você removeu, porque esse detalhe não está no que foi dado ao modelo.

Anunciando no Slack

Aprove uma entrada e ela pode ser postada em um canal do Slack no mesmo momento em que se torna pública. Conecte em Configurações, na aba Slack: crie um webhook de entrada no seu próprio workspace, escolha o canal e cole a URL. Nada é instalado do seu lado além desse webhook, e não pedimos nenhum acesso ao seu workspace.

A mensagem carrega o título da entrada, o texto como você aprovou, sua categoria e tags, e um link de volta para a entrada no seu changelog. O Markdown é traduzido para o que o Slack realmente renderiza, então uma entrada não chega mostrando seus próprios asteriscos.

A URL do webhook é uma credencial

Qualquer pessoa que tenha essa URL pode postar no canal, então a tratamos como uma senha: é armazenada, e depois disso nenhuma tela e nenhuma resposta de API a mostra novamente, incluindo sua própria exportação de dados. O que você vê depois é uma máscara, suficiente para distinguir dois webhooks e inútil para qualquer outra pessoa. Só aceitamos um endereço hooks.slack.com, então uma URL digitada errada ou substituída é recusada em vez de buscada.

Quando para de funcionar

Se você remover o app no Slack ou arquivar o canal, o webhook para de funcionar permanentemente. Percebemos isso na primeira mensagem recusada, desligamos os anúncios e informamos na aba Slack com o motivo e a data. É deliberado que não continuemos tentando silenciosamente: um changelog que ninguém anunciou parece exatamente com um que ninguém leu, e essa é uma diferença que vale a pena informar.

Pausando

Pausar interrompe os anúncios e mantém o webhook, então retomar é um clique em vez de outra volta pelo Slack. Desconectar remove a URL completamente. De qualquer forma, a publicação em si não é afetada: o Slack é um canal para onde seu changelog posta, nunca um portão que ele espera. Se o Slack estiver inacessível quando você aprovar algo, a entrada ainda é publicada e o anúncio é tentado novamente sozinho.

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

As mesmas entradas publicadas como um feed assinável, nos dois formatos que os leitores entendem: RSS 2.0 e JSON Feed 1.1. Ambos aceitam os mesmos filtros repos, category e tag do feed de changelog e carregam o mesmo Cache-Control e ETag. Nenhum pagina: um leitor consulta o topo do feed, então retornam apenas as entradas mais recentes, sem cursor.

O texto da entrada é o HTML sanitizado, envolvido em CDATA para RSS e como content_html para JSON Feed. O JSON Feed também carrega as cores das suas tags sob uma extensão com namespace _changelogapp; o RSS não, porque nenhum leitor as pintaria.

A página hospedada anuncia ambos como links rel="alternate", então um navegador ou leitor que chegue nela pode se inscrever sem que os caminhos sejam informados.

Uma entrada isolada

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

Retorna uma única entrada publicada, o mesmo objeto que o feed de changelog carrega em seu array data. É para onde os links permanentes nos feeds apontam, e é útil quando você tem um ID e não quer paginar pelo feed para encontrá-lo. Um ID desconhecido, ou de uma entrada que não está publicada, retorna 404 com o mesmo corpo de qualquer outro ID desconhecido.

O feed de markdown

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

As mesmas entradas publicadas como markdown simples, servido como text/markdown. Existe para leitores que não são navegadores: um LLM ou um agente respondendo "o que mudou recentemente neste produto" recebe o texto sem precisar analisar RSS ou percorrer JSON. Aceita os mesmos filtros repos, category e tag do feed de changelog, carrega o mesmo Cache-Control e ETag, e responde 304 a uma solicitação condicional exatamente como os outros dois.

Cada entrada é uma seção: o título como cabeçalho, depois uma única linha com a data, a categoria e quaisquer tags, depois o texto da entrada como foi escrito, depois o link Learn more se a entrada tiver um, depois seu link permanente. O documento abre com o título e a descrição do seu feed e leva de volta à página hospedada. Quando nada foi publicado ainda, diz isso em uma frase em vez de retornar um corpo vazio, então um leitor consegue distinguir isso de uma busca falha.

A página hospedada o anuncia como um link rel="alternate" com type text/markdown, ao lado dos links de RSS e JSON Feed, então um agente que buscou o HTML pode encontrá-lo sem que o caminho seja informado.

O que serve é o markdown que redigimos e você aprovou, não o HTML sanitizado. Isso é seguro como markdown, que é inerte, e é o motivo pelo qual esta resposta nunca é text/html. Se você mesmo o renderizar, escape-o como escaparia qualquer outro markdown não confiável: entradas redigidas a partir de um repositório público podem ser influenciadas por qualquer pessoa que consiga abrir um pull request ali.

Coletando feedback do seu próprio site

Adicione suas origens antes de testar isso

Este é o único endpoint do produto que escreve, então não aceita solicitações de qualquer lugar. Ele compara o cabeçalho Origin do navegador com uma lista de permissões por equipe, e essa lista começa vazia. Vazia significa rejeitar tudo, não permitir tudo. Até você adicionar a origem onde está incorporando, todo envio volta 403 com {"error":"origin_not_allowed"} e nada chega à sua caixa de entrada. Se seu formulário parece correto e ainda assim falha, quase sempre é por isso. Defina a lista com um PATCH autenticado para /v1/settings/feed carregando {"allowedOrigins": ["https://your-site.example"]}, e leia de volta com um GET no mesmo caminho, que responde com seu publicId, suas allowedOrigins, e o feedTitle e feedDescription que seus assinantes veem em um leitor de feed. Armazenamos cada origem exatamente na forma como um navegador a envia, então uma barra final ou uma porta padrão explícita no que você envia não é 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 precisa parecer um endereço de e-mail e ter 254 caracteres ou menos. message precisa ser não vazio e ter 2KB ou menos, medido em bytes UTF-8, não caracteres. O corpo JSON como um todo tem limite de 8KB. Há mais um campo, website: é uma armadilha para bots, então deixe de fora, ou envie vazio se o renderizar como um campo oculto do jeito que nosso widget faz.

Vale entender a armadilha antes de usá-la para depurar qualquer coisa. Se website chegar com algo escrito, respondemos 202 com um ID de envio de aparência perfeitamente normal e depois não fazemos nada, porque um bot que aprende que foi pego só tenta de novo de forma diferente. Essa é a resposta certa para um bot e confusa para você, então se o seu próprio formulário tiver um campo chamado website que um navegador possa preencher automaticamente, renomeie-o ou remova-o. Um envio que parece aceito e nunca aparece quase sempre é isso.

Um envio que aceitamos retorna 202 com um publicSubmissionId. Devolva isso à pessoa que enviou e guarde se puder: é a única forma de ela consultar o que aconteceu depois.

Os modos de falha são 400 com invalid_email ou invalid_message para o formato errado, 413 com email_too_large ou message_too_large para o formato certo mas grande demais, 429 com rate_limited acima de 5 envios por minuto ou 30 por hora de um endereço para um feed, 403 com origin_not_allowed, e 404 com not_found para um ID de feed que não reconhecemos.

Também há um limite diário por equipe de quanto trabalho downstream os envios podem disparar. Além dele, ainda aceitamos e armazenamos tudo que chega, simplesmente espera alguém da sua equipe olhar em vez de abrir algo sozinho.

Verificando um envio

GET/v1/public/YOUR_PUBLIC_ID/feedback/PUBLIC_SUBMISSION_ID

Responde com status, mais githubIssueUrl assim que existir uma issue para esse envio, mais shippedEntry carregando um título e um link assim que o trabalho estiver disponível. O endereço de e-mail de quem enviou nunca é lido do nosso banco de dados para esta rota, muito menos retornado, o que torna a resposta segura para renderizar em uma página que qualquer um pode ver. O ID é toda a credencial, trate-o como tal. Tem limite de 20 solicitações por minuto e 200 por hora, por endereço e feed.

Cache, CORS e solicitações condicionais

Ambos os feeds enviam Cache-Control: public, max-age=60, stale-while-revalidate=300 junto com um ETag forte. Devolva esse ETag como If-None-Match e um feed inalterado responde 304 sem corpo. Nenhum campo da resposta carrega um valor de relógio, então o ETag permanece estável quando renderizamos novamente dados que não mudaram, o que é o que torna esses 304 confiáveis.

Os dois feeds e a consulta de envio são leituras anônimas e respondem com Access-Control-Allow-Origin: *, então você pode chamá-los de qualquer origem, do curl ou de um passo de build. O POST de feedback é a exceção: responde com sua própria origem permitida e um Vary: Origin, nunca com um curinga. Navegadores fazem preflight nele, e um preflight sempre responde 204, a origem sendo permitida ou não, então não pode ser usado para sondar suas configurações.