Mudanças de API

Changelogs de webhook: o breaking change que ninguém pediu

6 min de leitura

Um changelog de API REST existe porque quem chama pode escolher rejeitar uma resposta que não entende, ou pelo menos registrar um erro alto o suficiente para alguém notar. Um receptor de webhook raramente faz uma coisa ou outra. Ele recebe um POST, lê os campos que espera, e se um campo se moveu, mudou de tipo ou sumiu, o endpoint ou trava em silêncio dentro de um job em segundo plano que ninguém observa ou, pior, continua rodando com um valor errado que nunca validou. O que é um breaking change cobre a definição geral; um payload de webhook precisa da própria resposta, porque o jeito de falhar é diferente do de um endpoint que alguém chama de propósito.

Por que uma mudança no payload de um webhook quebra diferente de uma mudança na resposta de uma API?

Porque a direção da requisição está invertida. Quem chama via REST inicia a chamada e pode adicionar um cabeçalho de versão, tentar de novo em um 4xx, ou ler um aviso de descontinuação na resposta. Um receptor de webhook não iniciou nada disso: seu servidor decidiu enviar, decidiu quando, e decidiu que forma o corpo teria. A única alavanca do receptor é a validação que ele escreveu quando a integração foi construída, e a maioria das integrações são construídas uma vez, funcionam, e ninguém revisita até quebrarem. Essa assimetria é o motivo inteiro pelo qual uma mudança no payload de um webhook merece mais cautela do que a mesma mudança em um corpo de resposta que quem chama pediu ativamente.

O que realmente conta como breaking change em um payload de webhook?

MudançaQuebra para a maioria dos receptores
Adicionar um campo novoNão, se os receptores ignoram campos desconhecidos (verifique essa suposição, não assuma)
Remover um campoSim, se algo o lê
Renomear um campoSim, funcionalmente idêntico a remover o antigo
Mudar o tipo de um campo (string para objeto)Sim, quase sempre
Reordenar campos no corpo JSONNão, para qualquer receptor que faz parse por chave, o que deveriam ser todos
Mudar o nome ou tipo do eventoSim, se os receptores filtram ou roteiam com base nisso

A linha “adicionar um campo é seguro” é a que os times mais confiam e a que mais vale a pena verificar em vez de assumir. Um parser JSON permissivo ignora campos desconhecidos por padrão, mas um receptor que faz deserialize para um esquema estrito, várias linguagens tipadas fazem isso sem configuração extra, pode rejeitar o payload inteiro assim que um campo inesperado aparece. Adicionar um campo só é seguro para seu webhook se você souber como os receptores fazem parse, não porque o JSON em si é permissivo.

Como versionar um payload de webhook?

Mais ou menos como para uma resposta de API, com uma diferença: o receptor nunca envia uma requisição, então não pode pedir uma versão, e quem envia precisa declará-la. Ela pode ir no corpo ou em um cabeçalho da própria requisição de entrega; as entregas do GitHub carregam X-GitHub-Event e X-GitHub-Hook-ID, e a especificação Standard Webhooks coloca seus metadados em cabeçalhos webhook-*. Um campo de versão no payload ("payload_version": 2) é a opção mais barata e funciona quando os receptores estão dispostos a ramificar com base nele. Um tipo de evento versionado (invoice.updated vira invoice.updated.v2 como um evento distinto ao qual um receptor se inscreve voluntariamente) exige mais trabalho para construir mas significa que a forma antiga continua chegando para quem nunca migrou, o que importa mais aqui do que em um endpoint REST porque você não pode ligar para cada receptor pedindo para atualizar. Uma configuração por assinatura, escolhida no registro do endpoint do webhook, antecipa a decisão em vez de ramificar a cada entrega, e é a escolha certa quando você já tem um registro de assinatura para anexá-la.

POST /endpoint-do-receptor
{
  "event": "invoice.updated",
  "payload_version": 2,
  "data": { "invoice_id": "inv_123", "status": "paid" }
}

Como você sabe sequer quem está escutando?

Pior do que a versão equivalente desse problema em um changelog de API, porque um webhook não tem um log de requisições recebidas do seu lado que nomeie quem chama; você só tem seu próprio log de entrega enviada, que diz que um endpoint recebeu um 200, não o que ele fez com o corpo. Rastreie pelo menos duas coisas: cada endpoint registrado com uma dona, a mesma disciplina que changelogs de API interna recomendam para consumidores internos, e sua taxa de falha de entrega por endpoint depois de uma mudança de payload. Um pico de respostas 4xx ou 5xx de um endpoint logo depois de uma mudança é a coisa mais próxima de um stack trace que você vai conseguir, e muitas vezes é o único sinal de que um receptor quebrou, porque o time que o opera pode não perceber por dias.

Um changelog de webhook deveria ser separado do changelog de API?

Uma seção separada na mesma página, não uma publicação separada. Um changelog de API já estabelece quem o lê e como se inscrever; uma mudança no payload de webhook pertence ao mesmo feed, etiquetada de forma clara o suficiente para que uma desenvolvedora do lado do receptor, escaneando por “isso afeta minha integração”, consiga filtrar por ela, porque quem consome webhook muitas vezes não tem outro motivo para checar um changelog geral de API e só vai encontrá-lo se alguém a direcionar até lá diretamente.

Como deveria ser uma janela de descontinuação razoável para um payload de webhook?

Mais longa que a descontinuação REST equivalente, porque a migração do lado do receptor geralmente significa que um segundo time, com quem talvez você não tenha contato direto, precisa notar, planejar e lançar sem urgência própria. Um mês é um mínimo razoável para um campo que o receptor plausivelmente ainda faz parse com uma biblioteca permissiva; três meses ou mais são mais seguros para remover um campo que um esquema estrito rejeitaria completamente. Envie a forma antiga e a nova juntas durante a janela quando for viável (o campo antigo status e seu substituto da versão 2 no mesmo payload), porque um receptor que lê o campo antigo continua funcionando sem tocar no código, e um que já migrou simplesmente ignora o campo de que não precisa mais.

FAQ

Consumidores de webhook precisam confirmar uma mudança de payload antes dela entrar no ar? Não existe mecanismo de confirmação por padrão, e é exatamente por isso que a janela de descontinuação importa mais aqui do que em uma API REST: ninguém confirma que está pronto, então a janela precisa ser longa o suficiente para que a maioria dos receptores migre no próprio ritmo antes da forma antiga desaparecer.

É sempre seguro adicionar campos desconhecidos sem aviso? Só depois de verificar, não assumir, que seus receptores fazem parse de forma permissiva. Uma entrada de changelog custa pouco e tira a incerteza; adicionar campos em silêncio na suposição de que “parsers JSON ignoram extras” quebra qualquer receptor com deserialização estrita.

Qual é o jeito mais rápido de detectar um receptor de webhook quebrado depois de uma mudança de payload? Uma taxa de falha de entrega por endpoint, observada nas horas logo depois da mudança. Ela não vai dizer o que quebrou, só que algo quebrou, mas é o sinal mais cedo e muitas vezes o único que você vai conseguir.

Lógica de retry ajuda receptores a sobreviver a uma mudança de payload? Não. Um retry reenvia o mesmo payload novo; ele não volta para uma forma que o receptor consiga fazer parse. Uma mudança de payload quebra um receptor na primeira entrega e em cada retry seguinte da mesma forma.


As afirmações técnicas deste artigo não foram revisadas de forma independente. Se algo estiver errado, avise a gente e vamos corrigir.

Relacionado na changeloop: Documentação para desenvolvedores, Comparativo de ferramentas de changelog

changeloop
O time por trás de um changelog que fecha o loop. Os usuários pedem algo, sua equipe entrega, quem pediu fica sabendo.