Webhooks
Notificações entre sistemas por HTTP, com assinatura, novas tentativas e entrega duplicada.
O problema: avisar outro sistema
Sua loja usa um provedor de pagamentos. O cliente paga com Pix, e o pagamento é aprovado alguns segundos depois, no sistema do provedor. Como a loja fica sabendo?
A loja pode perguntar de tempos em tempos: "o pagamento 981 foi aprovado?". É a consulta periódica do tópico de tempo real, agora entre dois servidores, com o mesmo custo: a maioria das perguntas volta sem novidade, e a novidade chega com atraso.
O webhook inverte a direção. A loja cadastra uma URL no provedor, e o provedor faz um pedido HTTP para ela quando algo acontece:
POST /webhooks/pagamentos HTTP/1.1
Host: loja.exemplo.com
Content-Type: application/json
webhook-id: msg_2KWPBgLlAfxdpx2AI54pPJ85f4W
webhook-timestamp: 1763164070
webhook-signature: v1,h2WnjGmWKj+dhK7D/6Yeu9HXHCmb9tmiP0qz+PvGAUY=
{"type":"pagamento.aprovado","data":{"pedido":981,"valor":"10.50"}}A loja responde com qualquer status 2xx para dizer "recebi". É só isso: um POST comum, de um servidor para outro. A dificuldade está em tudo o que pode dar errado no caminho.
Os cabeçalhos acima seguem o Standard Webhooks, uma especificação aberta adotada por vários provedores. Outros, como Stripe e GitHub, usam formatos próprios com as mesmas ideias; o nível Soluções compara os principais.
Quem garante que o aviso é verdadeiro
A URL do webhook é pública: qualquer pessoa que descobrir o endereço pode mandar um POST dizendo que o pedido 981 foi pago. Por isso, o emissor assina cada entrega.
Os dois lados combinam um segredo no cadastro. A cada entrega, o emissor calcula um HMAC-SHA256 (um código de autenticação feito com o segredo) sobre o identificador, o carimbo de tempo e o corpo, e envia o resultado no cabeçalho:
conteúdo assinado = webhook-id + "." + webhook-timestamp + "." + corpo
webhook-signature = "v1," + base64(HMAC-SHA256(segredo, conteúdo assinado))Quem recebe faz a mesma conta e compara. Sem o segredo, ninguém consegue produzir uma assinatura válida, e qualquer mudança no corpo, mesmo de um caractere, muda o resultado.
O corpo precisa ser o bruto
O detalhe que mais quebra integrações: a assinatura vale para os bytes exatos que chegaram. Se o código converte o JSON num objeto e depois o converte de volta em texto para conferir, o texto muda.
Testamos com um corpo de exemplo, assinado com o mesmo segredo:
| Texto | Assinatura confere? | |
|---|---|---|
| Original | {"id": "evt_1", "valor": 10.50, "cliente": "João"} | sim |
Node (JSON.stringify(JSON.parse(...))) | {"id":"evt_1","valor":10.5,"cliente":"João"} | não |
Python (json.dumps(json.loads(...))) | {"id": "evt_1", "valor": 10.5, "cliente": "João"} | não |
O Node tirou os espaços, e os dois transformaram 10.50 em 10.5. O Python ainda trocou o "ã" por ã. São o mesmo JSON, mas não os mesmos bytes, e a assinatura não confere. Muitos frameworks convertem o corpo automaticamente antes de chegar ao código, por isso a rota do webhook precisa pedir o corpo bruto.
O carimbo de tempo impede o reenvio
Imagine que alguém capturou uma entrega legítima, com assinatura válida. Sem mais nada, poderia reenviá-la amanhã, e ela passaria na conferência. O carimbo de tempo faz parte do conteúdo assinado: o receptor recusa entregas com mais de alguns minutos de diferença do relógio dele (5 minutos é o usual), e o atacante não consegue mudar o carimbo sem invalidar a assinatura.
Dentro da janela de tolerância, o identificador (webhook-id) resolve o resto: uma entrega com um identificador já recebido não é processada de novo.
O que acontece quando a entrega falha
O emissor considera a entrega feita só quando recebe um status 2xx. Qualquer outra coisa, um 500, um tempo esgotado ou a conexão recusada, faz o emissor tentar de novo mais tarde, com o mesmo webhook-id, por horas ou dias.
Isso é bom: o receptor pode ficar fora do ar sem perder avisos. Mas tem uma consequência: o mesmo aviso pode chegar mais de uma vez. E a repetição aparece até quando o receptor funcionou, como mostra o teste abaixo.
Montamos um emissor que espera 1 segundo pela resposta e tenta até 5 vezes, com intervalos crescentes. Do outro lado, três versões do receptor, cada uma levando 1,5 segundo para processar o aviso (o tempo de chamar outro serviço, por exemplo):
| Receptor | Tentativas | Processamentos | O emissor achou que |
|---|---|---|---|
| Processa e só depois responde | 5, todas por tempo esgotado | 5 | falhou |
| Confere o identificador, processa e responde | 2 | 1 | entregou |
| Grava o aviso, responde e processa depois | 1, respondida em 66 ms | 1 | entregou |
O primeiro receptor processou o mesmo pagamento cinco vezes, e o emissor ainda registrou a entrega como falha. Cada tentativa chegava, começava o trabalho, e o emissor desistia de esperar antes da resposta.
O segundo evitou a repetição, mas tem uma falha escondida: a segunda tentativa chegou enquanto a primeira ainda processava, encontrou o identificador já gravado e respondeu "recebi". Se o primeiro processamento falhasse depois disso, o aviso estaria perdido: o emissor já tinha a confirmação e não tentaria mais.
O terceiro é o desenho certo: gravar o aviso num lugar durável, responder em seguida e processar num trabalhador separado, que pode tentar de novo sem depender do emissor.
Fora de ordem
As novas tentativas também embaralham a ordem. No teste, o emissor tinha dois avisos para entregar: "pedido pago" e, um instante depois, "pedido enviado". A primeira tentativa do "pago" recebeu um 503; a do "enviado" deu certo. O "pago" só chegou na terceira tentativa, 5 segundos depois (com os intervalos encurtados para o teste).
O receptor viu "enviado" antes de "pago". Se ele simplesmente gravar o último status que chegou, o pedido fica como "pago" para sempre. Webhooks não garantem ordem; o nível Soluções mostra as formas de lidar com isso.
O lado de quem envia
Se você oferece webhooks, seus clientes cadastram URLs, e o seu servidor faz pedidos para elas. Isso abre uma porta para a rede interna: a falsificação de pedidos pelo servidor (server-side request forgery, SSRF).
A URL cadastrada pode ser http://169.254.169.254/..., o endereço de metadados das nuvens, de onde saem credenciais. Ou pode ser uma URL pública que responde com um redirecionamento para um endereço interno. Testamos esse segundo caso: um emissor com o fetch padrão do Node seguiu o redirecionamento para um serviço que só respondia em 127.0.0.1 e recebeu credencial-interna-123. Muitos painéis de webhooks mostram a resposta de cada entrega ao cliente, e aí o segredo interno chega a quem cadastrou a URL.
As defesas são três: resolver o nome e recusar endereços internos antes de cada entrega, não seguir redirecionamentos e, se possível, fazer as entregas a partir de uma rede sem acesso aos serviços internos.
Por que isso importa quando a IA escreve o código
Um assistente que escreve "a rota que recebe o webhook do pagamento" costuma gerar algo que funciona no primeiro teste e falha em produção:
- A assinatura conferida sobre o corpo já convertido em objeto, que não confere nunca, e alguém desliga a conferência "temporariamente".
- A comparação da assinatura com
==, em vez de uma comparação em tempo constante. - Nenhuma conferência do carimbo de tempo, e entregas antigas aceitas.
- O processamento inteiro antes da resposta, gerando tempo esgotado, novas tentativas e efeitos repetidos.
- Nenhum controle de identificadores já recebidos.
- Do lado de quem envia:
fetchdireto para a URL cadastrada, seguindo redirecionamentos, sem filtro de endereços internos.
O próximo nível compara os formatos de assinatura dos principais provedores e as formas de organizar quem recebe e quem envia.
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.
Webhooks em cinco minutos
Explique em no máximo cinco minutos de leitura o que é um webhook, para alguém que já fez APIs, mas nunca integrou com um provedor de pagamentos. Cubra só o essencial: o que é enviado, como quem recebe sabe que o aviso é verdadeiro, por que o mesmo aviso pode chegar mais de uma vez e o que fazer com isso. Use o exemplo de um pagamento aprovado avisado a uma loja. No final, me faça três perguntas curtas para eu verificar se entendi, e corrija as minhas respostas.
Assinatura de webhooks por dentro
Quero entender como funciona a assinatura de webhooks com HMAC. Aja como um mentor e vá por etapas, esperando a minha resposta em cada uma: 1. O que é um HMAC e por que ele prova a origem e a integridade da mensagem. 2. Por que a assinatura precisa ser conferida sobre o corpo bruto, com um exemplo de JSON que muda ao ser convertido e reconvertido. 3. Por que a comparação deve ser em tempo constante. 4. Como o carimbo de tempo dentro do conteúdo assinado impede o reenvio de uma entrega capturada. 5. Como funciona a troca de segredo com duas assinaturas no mesmo cabeçalho. Em cada etapa, me faça uma pergunta para verificar se entendi antes de seguir.
Novas tentativas e entregas repetidas
Quero entender por que webhooks chegam repetidos e fora de ordem. Explique: 1. Quando o emissor considera uma entrega feita e quando tenta de novo. 2. Por que um receptor que processa antes de responder pode processar o mesmo aviso várias vezes, mesmo sem erro nenhum. 3. Por que conferir o identificador antes de processar ainda pode perder avisos. 4. Como as novas tentativas mudam a ordem de chegada. 5. Por que o webhook deve ser tratado como aviso, e não como fonte da verdade. Depois me descreva três situações de produção e pergunte o que acontece em cada uma. Corrija as minhas respostas.
Os riscos de quem envia webhooks
Quero entender os riscos de segurança de oferecer webhooks para os clientes do meu sistema. Explique, com um exemplo de ataque para cada um: 1. Falsificação de pedidos pelo servidor (SSRF) com uma URL cadastrada apontando para um endereço interno ou de metadados da nuvem. 2. Uma URL pública que redireciona para um endereço interno. 3. Um nome que muda de endereço entre a conferência e a conexão (DNS rebinding). 4. Formas alternativas de escrever um IP, como decimal, hexadecimal e IPv4 dentro de IPv6. 5. A resposta de um serviço interno exibida no painel de entregas do cliente. Para cada um, diga a defesa e como testar.