DevGuideTK
← Trilha Backend

Webhooks

Notificações entre sistemas por HTTP, com assinatura, novas tentativas e entrega duplicada.

As decisões de webhooks se dividem entre os dois lados. Quem recebe decide como confirmar a origem, como não repetir o efeito e como lidar com a ordem. Quem envia decide como garantir a entrega e como proteger a própria rede.

Como confirmar a origem

FormaComo funcionaQuando usar
HMAC com segredo compartilhadoEmissor e receptor conhecem o mesmo segredo; a assinatura vai num cabeçalhoO padrão do mercado; serve para quase todos os casos
Assinatura assimétricaO emissor assina com uma chave privada (Ed25519, por exemplo); o receptor confere com a chave públicaQuando o receptor não deve guardar um segredo capaz de produzir assinaturas
TLS mútuo (mTLS)O emissor apresenta um certificado de cliente na conexãoIntegrações entre empresas com infraestrutura própria de certificados
Lista de IPs do emissorO receptor só aceita conexões de faixas publicadas pelo emissorComo camada extra, nunca sozinha: as faixas mudam e podem ser compartilhadas
Token na URLUm valor secreto no caminho ou na consultaEvite: aparece em logs e não protege o corpo contra alteração

O HMAC resolve a autenticidade e a integridade com uma conta simples, disponível na biblioteca padrão de todas as linguagens. A assinatura assimétrica é o próximo passo quando o emissor quer garantir que nem o receptor consiga forjar uma entrega. O Standard Webhooks prevê as duas: v1, para HMAC e v1a, para Ed25519.

Os formatos dos principais emissores

Cada emissor tem o seu formato, e a biblioteca oficial dele costuma já fazer a conferência:

EmissorCabeçalhoO que é assinadoCarimbo de tempo
Standard Webhookswebhook-signature: v1,<base64>identificador, carimbo e corpoSim, em webhook-timestamp
StripeStripe-Signature: t=<carimbo>,v1=<hex>carimbo e corpoSim, dentro do próprio cabeçalho
GitHubX-Hub-Signature-256: sha256=<hex>só o corpoNão
ShopifyX-Shopify-Hmac-Sha256: <base64>só o corpoNão

Quando o carimbo não faz parte da assinatura, como no GitHub e na Shopify, uma entrega capturada continua válida para sempre. A defesa passa a ser guardar os identificadores de entrega (X-GitHub-Delivery, X-Shopify-Webhook-Id) e recusar os repetidos.

Prefira a biblioteca oficial do emissor quando existir. Ela acerta os detalhes: a codificação (hexadecimal ou base64), a ordem dos campos, a tolerância de tempo e as várias assinaturas durante a troca de segredo.

Como receber sem repetir o efeito

O teste do nível Conceito comparou três desenhos de receptor, com um emissor que espera 1 segundo pela resposta e um processamento de 1,5 segundo:

DesenhoProcessamentos em 5 tentativas possíveisProblema
Processa e responde5Repete o efeito a cada tentativa, e o emissor registra falha
Confere o identificador, processa e responde1Confirma a repetição antes de o primeiro processamento terminar; se ele falhar, o aviso se perde
Grava, responde e processa depois1Nenhum: o aviso fica guardado até ser processado

O desenho recomendado tem três peças:

  1. Conferir e gravar: conferir a assinatura e gravar o aviso numa tabela com o webhook-id como chave única. Uma entrega repetida bate na chave e é ignorada.
  2. Responder 2xx em seguida, em milissegundos, inclusive para os repetidos. Para o emissor, um repetido confirmado é uma entrega encerrada.
  3. Processar num trabalhador, que lê a tabela, faz o trabalho e marca o aviso como processado. Se falhar, tenta de novo por conta própria.

A tabela pode ser trocada por uma fila, como mostra o tópico de filas, desde que a gravação seja durável antes da resposta. O processamento em si também precisa ser idempotente: o trabalhador pode cair depois de cobrar e antes de marcar o aviso, como mostra o tópico de idempotência.

Como lidar com a ordem

AbordagemComo funcionaCusto
Aviso magro e consultaO aviso diz só "o pedido 981 mudou"; o receptor consulta a API do emissor para saber o estado atualUm pedido a mais por aviso, e uma API de consulta do lado do emissor
Versão no eventoCada evento traz uma versão ou um carimbo do recurso; o receptor ignora o que for mais antigo que o gravadoO emissor precisa fornecer a versão
Transições válidasO receptor só aceita mudanças possíveis: de "enviado" não se volta para "pago"Lógica de estados no receptor; não serve para todo tipo de dado
Aviso completo, sem cuidadoO receptor grava o que chegar por últimoFunciona até a primeira nova tentativa fora de ordem

O aviso magro é a forma mais robusta: a ordem deixa de importar, e um aviso falsificado não consegue mudar nada, porque o dado vem da API do emissor. A versão no evento é o meio-termo, e se resume a uma condição na gravação:

UPDATE pedidos SET status = $2, versao = $3
 WHERE id = $1 AND versao < $3; -- eventos antigos não sobrescrevem os novos

Como garantir a entrega, do lado de quem envia

PeçaPara que serve
Fila de saída (outbox)A entrega é gravada na mesma transação da mudança que gerou o evento; um processo separado faz os envios
Novas tentativas espaçadasIntervalos crescentes, com variação aleatória, por horas ou dias
Tempo limite curtoAlguns segundos por entrega, para um receptor lento não travar os envios
Registro de cada entregaStatus, tempo e horário de cada tentativa, visíveis ao cliente
Reenvio manualO cliente pede de novo uma entrega que falhou, depois de corrigir o lado dele
Desativar destinos mortosDepois de dias de falhas, o destino é desativado e o cliente é avisado
Troca de segredoDurante a troca, o emissor assina com o segredo antigo e o novo, e o cabeçalho leva as duas assinaturas

A fila de saída resolve o problema de enviar o aviso fora da transação: se a aplicação grava "pago" e cai antes de enviar o webhook, o aviso nunca sai. Gravando a entrega junto com a mudança, ou as duas acontecem, ou nenhuma. É o mesmo padrão do tópico de filas.

Construir ou usar um serviço

OpçãoQuando usar
Construir com a fila de saída e um trabalhadorPoucos tipos de evento, poucos clientes, sem painel de entregas para o cliente
Serviço dedicado, gerenciado ou de código aberto (Svix, Convoy, Hookdeck)Webhooks como produto: muitos clientes, painel de entregas, reenvio, troca de segredo

O envio em si é a parte fácil. O trabalho está no painel, no registro de tentativas, no reenvio, na desativação e no suporte aos clientes cujas integrações falham.

Como proteger a rede de quem envia

RiscoDefesa
URL cadastrada apontando para um endereço interno ou de metadadosResolver o nome e recusar faixas internas, antes de cada entrega
URL pública que redireciona para um endereço internoNão seguir redirecionamentos; um 3xx conta como falha
Nome que muda de endereço entre a conferência e a conexão (DNS rebinding)Conectar no endereço conferido, ou sair por um proxy de saída que faz a filtragem
A resposta do serviço interno exibida no painel do clienteLimitar e filtrar o que o painel mostra
Entregas em HTTP sem criptografiaExigir HTTPS nas URLs cadastradas

A conferência no cadastro não basta: o nome pode passar a apontar para outro endereço depois. A defesa mais forte é estrutural: os trabalhadores de entrega rodam numa rede sem rota para os serviços internos, ou saem por um proxy que bloqueia faixas internas.

Resumo da decisão

  1. Assinatura HMAC com carimbo de tempo, conferida sobre o corpo bruto, com a biblioteca do emissor quando houver.
  2. Gravar, responder e processar depois, com o identificador da entrega como chave única.
  3. Ordem: aviso magro e consulta, ou uma versão no evento.
  4. Conciliação periódica, porque o webhook é um aviso, e não a fonte da verdade.
  5. Ao enviar: fila de saída na mesma transação, tentativas espaçadas, registro e reenvio.
  6. Ao enviar: filtro de endereços internos em cada entrega, sem seguir redirecionamentos.

O próximo nível mostra o receptor nas cinco linguagens, testado com assinaturas geradas pela biblioteca oficial do Standard Webhooks, e o emissor com a fila de saída.

Prompts para aprofundar

Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.

  • Desenhar o recebimento de webhooks

    Vou receber webhooks de um provedor e quero desenhar o recebimento do jeito certo.
    
    Pergunte qual é o provedor, o que os avisos disparam no meu sistema, qual linguagem e banco eu uso e se já existe uma fila.
    
    Depois recomende, explicando o porquê:
    1. Como conferir a assinatura (a biblioteca oficial do provedor, se houver).
    2. Onde gravar o aviso antes de responder, com o identificador como chave única.
    3. Como processar fora da rota, e como tornar o processamento idempotente.
    4. Como lidar com avisos fora de ordem: aviso magro e consulta, versão no evento ou transições válidas.
    5. Como montar a conciliação periódica com a API do provedor.
  • Oferecer webhooks aos meus clientes

    Quero que o meu sistema envie webhooks para os meus clientes.
    
    Pergunte quantos clientes e eventos por dia eu espero, qual linguagem e banco eu uso e se os clientes vão precisar de um painel de entregas.
    
    Compare para o meu caso construir com uma fila de saída e um trabalhador, ou usar um serviço dedicado, e explique:
    1. O formato da assinatura (por exemplo, o Standard Webhooks) e a troca de segredo.
    2. A política de novas tentativas, o tempo limite e quando desativar um destino.
    3. O registro de entregas e o reenvio manual.
    4. As proteções contra SSRF.
    5. O formato dos eventos: aviso magro ou completo, e com versão.
  • Escolher o formato dos eventos

    Estou definindo o formato dos eventos de webhook do meu sistema e tenho dúvidas entre mandar só um aviso ("o pedido 981 mudou") ou os dados completos.
    
    Pergunte que tipo de dado os eventos carregam e o que os clientes fazem com eles.
    
    Compare as duas formas quanto a:
    1. Ordem de chegada e novas tentativas.
    2. Segurança, se o aviso for forjado ou vazar.
    3. Carga na minha API.
    4. Facilidade de uso para quem integra.
    5. Evolução do formato ao longo do tempo.
    
    Depois me ajude a escrever o formato de dois eventos do meu sistema, com identificador, tipo, carimbo de tempo e versão do recurso.