Como projetar webhooks confiáveis com retries e idempotência

Veja onde persistir, confirmar e repetir uma operação, com exemplos de créditos por fatura e uma proposta de alertas de feedback.

Atualizado em

Cinco envelopes de evento idênticos seguem caminhos em azul-marinho e cobalto até um único registro, com um retorno âmbar e um detalhe verde-lima.

Um webhook avisa sua aplicação de que algo aconteceu em outro sistema. A dificuldade aparece quando a conexão falha: sua aplicação perdeu o evento, conseguiu salvá-lo ou já alterou a conta do cliente? Tentar de novo é necessário em alguns desses casos e perigoso em outros.

Este guia é para quem está construindo uma integração em uma aplicação existente. Você vai entender quando responder à requisição, o que persistir e quais operações precisam ser seguras para repetir. A arquitetura de referência usa o banco da aplicação e um worker em segundo plano. Não exige broker de mensagens nem um provedor de nuvem específico.

O exemplo é um serviço hipotético de exportação. Cada fatura paga libera 100 créditos para um workspace. A regra é uma concessão inicial por fatura elegível, mesmo que notificações diferentes ou rotinas de reparo se refiram a ela. Um e-mail de confirmação é um segundo efeito, opcional.

Comece pelas fronteiras do sistema

Arquitetura de webhook com transação T1 de recebimento e T2 de processamento local, mais saída externa opcional.

Figura 1. Inbox e registros de negócio ficam no mesmo banco. O worker busca diretamente os eventos pendentes, sem uma passagem adicional para uma fila. O caminho tracejado só é necessário quando existe efeito externo.

A inbox é uma lista persistente de eventos recebidos. Ela permite recuperar o trabalho após reiniciar o processo. A outbox guarda ações que precisam sair da aplicação; só entra no desenho quando uma mudança confirmada deve provocar uma ação externa. Cada uma protege um lado do processamento.

Confirme apenas o trabalho que consegue recuperar

O endpoint começa verificando a autenticidade conforme o mecanismo de assinatura do emissor. Depois valida o tipo do evento e os identificadores necessários. Preserve o corpo original quando a verificação exigir isso. Associe a conta de origem verificada ao seu tenant; um ID de workspace no payload não comprova, sozinho, autorização. O guia oficial de assinaturas da Stripe traz um exemplo concreto desse contrato.

Para aceitar o evento, insira uma linha na inbox e confirme a transação antes de devolver sucesso HTTP. Guarde a chave do evento, os dados normalizados ou o payload protegido, o estado do processamento, a quantidade de tentativas, a próxima tentativa e o último erro. Restrinja acesso e retenha apenas o necessário. Evite copiar payloads para logs gerais.

Essa resposta significa 'tenho trabalho persistido e recuperável'. Os créditos ainda podem não ter sido liberados. Se o commit falhar ou seu resultado ficar incerto, não confirme o recebimento. Uma nova entrega poderá consultar a chave persistida e resolver a dúvida. Rejeite assinaturas inválidas; tipos autenticados que sua integração decidiu ignorar podem receber sucesso, conforme o contrato do emissor.

Ao receber uma duplicata, confirme que a linha existente está persistida e corresponde ao mesmo evento e origem. Então responda ao emissor. Se a linha ainda estiver pendente ou com erro, preserve seu caminho de recuperação; não a marque como concluída. Conteúdo incompatível com um ID já recebido deve ser separado para investigação, sem sobrescrever silenciosamente o original.

Separe a identidade do evento da identidade da operação

Chave do evento: (origem, conta de origem, ID do evento). Responde à pergunta 'já aceitei este evento?'. Use o identificador que o emissor mantém nas reentregas, não um ID novo para cada tentativa HTTP. Os campos devem ser não nulos e ter uma restrição de unicidade no banco.

Chave de negócio: (origem, conta de origem, ID da fatura, concessão-inicial-de-créditos). Responde à pergunta 'esta fatura já liberou seus créditos?'. Dois eventos diferentes, um webhook e um reparo manual podem representar a mesma operação. Usar apenas o ID da assinatura bloquearia indevidamente os créditos de faturas futuras.

Quem deve resolver a concorrência é o banco, não uma consulta prévia do tipo 'já existe?'. As restrições de unicidade do PostgreSQL oferecem esse mecanismo. Preserve o registro de negócio enquanto aquela operação precisar continuar não repetível. Um cache de deduplicação com validade curta não sustenta essa regra.

O exemplo pressupõe uma associação confiável entre fatura, workspace e direito aos créditos. Valide elegibilidade e quantidade antes de aplicá-los. Estornos e correções precisam de operações próprias e identificadas; não são outra execução da concessão inicial.

Confirme o efeito local e seu resultado na mesma transação

Neste desenho enxuto, cada worker abre uma transação curta, bloqueia uma linha pendente da inbox cuja próxima tentativa já venceu e mantém o bloqueio até o commit. Outros workers pulam as linhas bloqueadas. Se a conexão do worker cair, a transação incompleta é desfeita e o bloqueio é liberado. Outro worker pode retomar o evento. Uma reserva com prazo, ou lease, também funciona, mas exige expiração, verificação de posse e proteção contra workers atrasados. O PostgreSQL documenta esse uso de SKIP LOCKED para consumidores de uma fila.

O pseudocódigo abaixo mostra a fronteira transacional. A inserção da concessão informa se criou uma linha nova. O incremento do saldo é atômico e todas as tabelas participam da mesma transação no banco.

BEGIN
  evento = bloquear_pendente_com_retry_vencido(pular_bloqueadas = true)
  se nenhum evento: COMMIT e encerrar

  validar fatura, tenant e quantidade fixa de créditos
  criada = inserir_concessao_se_ausente(chave_negocio, 100)
  se criada:
    incrementar_saldo_atomicamente(100)
    inserir_outbox_se_necessario(chave_negocio + ':confirmacao')
  senao:
    verificar_se_concessao_existente_corresponde_a_operacao()

  marcar_inbox_concluida(evento.chave)
COMMIT

O rollback desfaz a nova concessão, a alteração de saldo, a intenção de envio e a marca de conclusão. O commit preserva tudo junto. Se o worker perder a resposta do commit, deve consultar o estado persistido, em vez de somar créditos novamente. Um evento diferente para a mesma fatura pode chegar depois, mas a chave de negócio única impede outro incremento.

Não faça chamadas remotas mantendo essa transação aberta. Se a elegibilidade depender de uma consulta externa, obtenha os dados antes e revalide o estado local relevante ao confirmar. Consultas sensíveis à ordem precisam também da coordenação descrita adiante.

Em falhas transitórias, faça rollback e grave o agendamento da próxima tentativa em outra transação curta, apenas se a linha continuar pendente. Use espera exponencial com limite e jitter, uma variação aleatória que evita novas tentativas simultâneas. Defina um orçamento de tentativas ou de tempo. Separe erros de validação e tentativas esgotadas para revisão. Esse controle nunca deve sobrescrever uma conclusão confirmada por outro worker.

Quando dá para simplificar

Se todo o efeito for uma alteração local rápida e couber com folga no prazo do emissor, o próprio endpoint pode executar uma transação com deduplicação, atualização de negócio e conclusão, respondendo depois do commit. Não precisa de worker nem outbox. Use inbox e worker quando houver lentidão, picos ou necessidade de recuperação independente do emissor. Acrescente um broker quando escala ou isolamento justificarem essa escolha.

Acompanhe uma resposta perdida

Confirmação perdida, evento repetido, deduplicação na inbox, commit de processamento e reinício seguro.

Figura 2. A resposta se perde depois do commit do recebimento. A reentrega encontra a mesma linha na inbox. O processamento confirma uma única concessão. Outro evento para a mesma fatura é barrado pela chave de negócio, não pelo ID do evento.

Existem dois ciclos de retry, ou nova tentativa. O emissor repete a entrega HTTP. Seu worker repete o processamento ainda incompleto, mesmo depois de o recebimento ter sido confirmado. Um 2xx não encerra sua responsabilidade pela linha nem elimina a necessidade de resolver falhas permanentes.

Um efeito externo depende de outro contrato

Um serviço de e-mail não participa da transação do seu banco. Enviar antes do commit pode confirmar uma concessão que será desfeita. Enviar depois, sem registrar a intenção, pode perder o e-mail se o processo cair nesse intervalo. Quando a confirmação precisa acompanhar a concessão, grave a linha da outbox na mesma transação. Um despachante consulta as saídas pendentes e chama o serviço de e-mail.

O despachante pode cair depois de o serviço aceitar a mensagem e antes de marcar a saída como enviada. Uma tentativa posterior pode enviá-la novamente. Se o destino oferecer idempotência, reutilize uma chave estável da operação e confira seu escopo, prazo de retenção e regras de repetição. Sem esse suporte, escolha uma política explícita para resultados incertos: consultar registros de entrega quando possível, aceitar duplicatas em mensagens de baixo risco ou exigir revisão antes de repetir ações de alto impacto.

O padrão de outbox transacional fecha a lacuna entre confirmar o estado local e registrar o trabalho de saída. Ele não garante entrega exatamente uma vez de ponta a ponta. O despachante e o destino continuam precisando tratar suas próprias falhas.

Aplicando o desenho aos alertas de feedback

Imagine um SaaS que queira receber os novos feedbacks do Sinalio no seu painel interno e, opcionalmente, avisar a equipe em outro canal. As fronteiras são as mesmas. A operação de negócio passa a ser “criar o alerta inicial deste feedback”, em vez de “liberar créditos desta fatura”.

A configuração de webhooks do Sinalio permite definir um receptor para notificações assinadas de novos feedbacks. Em 4 de outubro de 2026, o site público ainda apresentava webhooks em validação no Alpha. O fluxo abaixo propõe como a aplicação cliente pode consumir essas notificações. Ele não especifica nomes oficiais de eventos, campos do payload, procedimento de verificação da assinatura ou regras de retry, que precisam ser confirmados antes da implementação.

Receptor proposto de feedbacks Sinalio, com alerta local único e envio externo opcional implementado pelo cliente.

Figura 3. Fluxo proposto entre Sinalio e cliente. Endpoint receptor, banco, worker e integração de saída pertencem ao SaaS cliente. Um alerta apenas local não exige envio externo.

Confirmado o contrato, o endpoint do cliente autentica o webhook e salva o recebimento na inbox. O worker registra ou referencia o feedback e cria um único alerta. Para o alerta inicial, uma chave de negócio proposta seria (projeto Sinalio, ID estável do feedback, alerta-inicial). São identidades conceituais, não nomes oficiais de campos do payload. Se houver destinos diferentes, inclua o destino na identidade de cada operação de saída.

Uma reentrega deve encontrar o mesmo recebimento. Eventos diferentes sobre o mesmo feedback não devem gerar outro alerta inicial. Um alerta posterior legítimo, como um escalonamento definido separadamente, precisa de outra chave de operação. Referência do feedback, alerta local, resultado da inbox e eventual intenção de saída devem ser confirmados juntos.

Se o alerta aparecer apenas no painel do próprio SaaS, o registro local pode ser o efeito concluído. Para avisar um destino externo da equipe, a integração implementada pelo cliente pode despachar uma entrada da outbox. Um envio com resultado incerto continua dependendo de idempotência no destino ou de uma política explícita entre duplicação e omissão.

Antes de implementar, confirme catálogo e schema dos eventos, identidades estáveis de evento e feedback, escopo de projeto, verificação de autenticidade, timeout, retries, opções de replay e elegibilidade por plano. O texto comercial público não permite deduzir esses contratos.

Retries não colocam os eventos em ordem

No histórico de créditos, as faturas A e B representam concessões legítimas diferentes. Processar B primeiro não pode eliminar A. Já uma projeção do estado atual tem outra regra: uma notificação antiga de assinatura ativa não deve sobrescrever um cancelamento mais recente. Deduplicar os dois eventos não determina qual estado é o atual.

Se o provedor oferecer uma versão monotônica autoritativa, condicione as atualizações a ela. Caso contrário, reconcilie o estado atual consultando o provedor e coordene todo o ciclo de consulta e gravação por recurso, evitando que uma consulta anterior sobrescreva um resultado posterior. Respeite as garantias de consistência da API e execute reconciliação para corrigir divergências. Ordem de chegada e timestamp comum não são números de sequência universais.

Confira quem é responsável pela recuperação

Projete o processamento para tolerar tentativas repetidas, sem assumir que todo provedor garante a entrega. At-least-once é uma garantia de uma ou mais entregas nas condições do transporte; não significa efeito de negócio único. Alguns provedores repetem automaticamente; outros dependem de uma rotina sua ou de um operador. Sua arquitetura local não processa um evento que nunca recebeu nem recuperou.

A Stripe, por exemplo, documenta duplicatas, ausência de garantia de ordem e retries automáticos por até três dias no modo live. O GitHub não reenvia webhooks com falha automaticamente. São políticas de transporte que sua integração precisa conhecer. Inbox, chave de negócio e fronteira transacional continuam sendo decisões da aplicação.

Ao adicionar um broker, evite 'salvar inbox e depois publicar' como duas gravações sem coordenação. Uma queda entre elas deixa trabalho aceito sem processamento. Mantenha a consulta ao banco, use outbox transacional ou captura de alterações para essa passagem, ou adote deliberadamente uma fila durável como fonte do trabalho pendente. Verifique confirmação, retenção e reentrega da fila escolhida.

Teste as falhas que deixam o resultado incerto

  • Entregue o mesmo evento simultaneamente: uma linha na inbox e uma concessão.
  • Envie IDs diferentes para a mesma fatura: dois recebimentos, uma concessão e uma intenção inicial de confirmação.
  • Interrompa após o commit do recebimento: o trabalho pendente continua localizável.
  • Interrompa antes e depois do commit de negócio: rollback ou uma concessão confirmada, sem novo incremento.
  • Perca a resposta do serviço externo: exercite a política escolhida para envio incerto.
  • Inverta atualizações de estado e execute replay junto ao fluxo normal: sem sobrescrita antiga nem concessão duplicada.

Monitore a idade do evento pendente mais antigo, tentativas esgotadas, erros e divergências de reconciliação. Taxa de sucesso HTTP não basta. Registre as chaves de evento e de negócio em logs estruturados para rastrear uma fatura até o resultado. O replay deve passar pela mesma operação idempotente, sem contorná-la.

Comece pelo menor desenho que preserva a regra do produto. Acrescente cada fronteira de forma consciente e mantenha o trabalho incompleto visível. O resultado útil é saber o que uma entrega repetida pode fazer e como recuperar uma tentativa que falhou.

Escrito por

João Müller