Integração por webhook sem perder evento

Webhooks falham de três jeitos: evento perdido, evento duplicado e falha silenciosa. Idempotência, reprocesso e um contador de reconciliação resolvem os três.

Webhook é o mecanismo mais comum de integração entre sistemas: quando algo acontece no sistema A (um pagamento, um pedido, uma mensagem), ele faz uma chamada HTTP para o sistema B avisando. É simples de implementar no caminho feliz, e é por isso que quase toda integração por webhook em produção tem o mesmo trio de defeitos latentes: perde evento quando B está fora do ar, duplica efeito quando A reenvia e falha em silêncio quando algo no meio muda.

O padrão que descrevemos aqui cabe em qualquer stack e elimina os três. Nada dele é novidade; a diferença entre as integrações que operam anos sem incidente e as que perdem pedido de madrugada é aplicá-lo por inteiro.

Assuma a entrega "pelo menos uma vez"

Todo emissor sério de webhooks reenvia quando não recebe HTTP 2xx, seja por timeout, por erro 500 ou por queda de rede. Consequência direta: o mesmo evento vai chegar mais de uma vez, e o seu lado precisa ser idempotente (processar a duplicata sem repetir o efeito). A implementação mais barata e mais confiável é uma restrição de unicidade no banco, usando o identificador que o emissor manda:

CREATE TABLE eventos_recebidos (
  origem     text        NOT NULL,
  evento_id  text        NOT NULL,
  payload    jsonb       NOT NULL,
  status     text        NOT NULL DEFAULT 'pendente',
  recebido_em timestamptz NOT NULL DEFAULT now(),
  PRIMARY KEY (origem, evento_id)
);

O INSERT que conflita nessa chave é uma duplicata: responda 200 e descarte. Checagem de duplicata em memória ou em cache deixa de proteger no primeiro reinício do processo ou na segunda réplica do serviço.

Grave primeiro, processe depois

O erro estrutural mais comum é processar o evento dentro do request do webhook: chamar a API do ERP, enviar o e-mail, baixar o estoque, e só então responder. Se qualquer passo demorar, o emissor estoura o timeout (tipicamente 5 a 10 segundos), marca falha e reenvia, agora com o efeito parcialmente aplicado.

Inverta a ordem. O handler do webhook faz três coisas: valida a assinatura, insere a linha em eventos_recebidos e responde 200. Um worker separado consome os pendentes e aplica os efeitos. O request passa a durar milissegundos, o reenvio vira duplicata inofensiva, e um pico de mil eventos vira fila em vez de avalanche.

A validação de assinatura é obrigatória nesse desenho: o endpoint é público, e sem verificar o HMAC (o hash que o emissor calcula com um segredo compartilhado) qualquer um que descubra a URL fabrica eventos.

Reprocesso com desistência explícita

O worker vai falhar em algum momento: a API de destino cai, o dado vem malformado. Retentativa imediata em loop só transforma falha passageira em martelo. O espaçamento exponencial dá tempo do mundo se recuperar:

tentativa 1: imediata      tentativa 4: +30 min
tentativa 2: +1 min        tentativa 5: +4 h
tentativa 3: +5 min        depois: status 'morto' + alerta

O estado final importa tanto quanto as tentativas. Evento que esgotou as tentativas muda para um status terminal e dispara alerta para alguém olhar; nenhum evento é descartado sem registro. No nosso histórico de sustentação, o defeito mais recorrente em integração alheia é exatamente este: o caminho de erro existe no código, mas nunca foi exercitado, e o dia em que roda pela primeira vez é o dia do incidente. Teste o caminho de falha como se testa o de sucesso: derrube o destino de propósito em homologação e assista ao reprocesso funcionar.

Reconciliação para o que ninguém previu

Idempotência e reprocesso cobrem as falhas conhecidas. Sobra a categoria que nenhum código prevê: o emissor que parou de enviar, o segredo rotacionado, a URL alterada por engano. Para essa categoria o mecanismo é contábil. Uma rotina diária compara os totais dos dois lados:

eventos emitidos por A (via API de consulta, últimas 24h):  1.412
eventos registrados em B:                                   1.409
diferença: 3  →  alerta, com os IDs faltantes no corpo

O mecanismo é deselegante e cumpre o papel: as três linhas de diferença de hoje são os pedidos perdidos que, sem a reconciliação, alguém descobriria no fechamento do mês.

Se existe uma integração na sua operação em que ninguém saberia dizer quantos eventos se perderam no último trimestre, é por ela que vale começar: contato@castelapi.com.br.