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
| Forma | Como funciona | Quando usar |
|---|---|---|
| HMAC com segredo compartilhado | Emissor e receptor conhecem o mesmo segredo; a assinatura vai num cabeçalho | O padrão do mercado; serve para quase todos os casos |
| Assinatura assimétrica | O emissor assina com uma chave privada (Ed25519, por exemplo); o receptor confere com a chave pública | Quando 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ão | Integrações entre empresas com infraestrutura própria de certificados |
| Lista de IPs do emissor | O receptor só aceita conexões de faixas publicadas pelo emissor | Como camada extra, nunca sozinha: as faixas mudam e podem ser compartilhadas |
| Token na URL | Um valor secreto no caminho ou na consulta | Evite: 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:
| Emissor | Cabeçalho | O que é assinado | Carimbo de tempo |
|---|---|---|---|
| Standard Webhooks | webhook-signature: v1,<base64> | identificador, carimbo e corpo | Sim, em webhook-timestamp |
| Stripe | Stripe-Signature: t=<carimbo>,v1=<hex> | carimbo e corpo | Sim, dentro do próprio cabeçalho |
| GitHub | X-Hub-Signature-256: sha256=<hex> | só o corpo | Não |
| Shopify | X-Shopify-Hmac-Sha256: <base64> | só o corpo | Nã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:
| Desenho | Processamentos em 5 tentativas possíveis | Problema |
|---|---|---|
| Processa e responde | 5 | Repete o efeito a cada tentativa, e o emissor registra falha |
| Confere o identificador, processa e responde | 1 | Confirma a repetição antes de o primeiro processamento terminar; se ele falhar, o aviso se perde |
| Grava, responde e processa depois | 1 | Nenhum: o aviso fica guardado até ser processado |
O desenho recomendado tem três peças:
- Conferir e gravar: conferir a assinatura e gravar o aviso numa tabela com o
webhook-idcomo chave única. Uma entrega repetida bate na chave e é ignorada. - Responder 2xx em seguida, em milissegundos, inclusive para os repetidos. Para o emissor, um repetido confirmado é uma entrega encerrada.
- 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
| Abordagem | Como funciona | Custo |
|---|---|---|
| Aviso magro e consulta | O aviso diz só "o pedido 981 mudou"; o receptor consulta a API do emissor para saber o estado atual | Um pedido a mais por aviso, e uma API de consulta do lado do emissor |
| Versão no evento | Cada evento traz uma versão ou um carimbo do recurso; o receptor ignora o que for mais antigo que o gravado | O emissor precisa fornecer a versão |
| Transições válidas | O 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 cuidado | O receptor grava o que chegar por último | Funciona 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 novosComo garantir a entrega, do lado de quem envia
| Peça | Para 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çadas | Intervalos crescentes, com variação aleatória, por horas ou dias |
| Tempo limite curto | Alguns segundos por entrega, para um receptor lento não travar os envios |
| Registro de cada entrega | Status, tempo e horário de cada tentativa, visíveis ao cliente |
| Reenvio manual | O cliente pede de novo uma entrega que falhou, depois de corrigir o lado dele |
| Desativar destinos mortos | Depois de dias de falhas, o destino é desativado e o cliente é avisado |
| Troca de segredo | Durante 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ção | Quando usar |
|---|---|
| Construir com a fila de saída e um trabalhador | Poucos 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
| Risco | Defesa |
|---|---|
| URL cadastrada apontando para um endereço interno ou de metadados | Resolver o nome e recusar faixas internas, antes de cada entrega |
| URL pública que redireciona para um endereço interno | Nã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 cliente | Limitar e filtrar o que o painel mostra |
| Entregas em HTTP sem criptografia | Exigir 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
- Assinatura HMAC com carimbo de tempo, conferida sobre o corpo bruto, com a biblioteca do emissor quando houver.
- Gravar, responder e processar depois, com o identificador da entrega como chave única.
- Ordem: aviso magro e consulta, ou uma versão no evento.
- Conciliação periódica, porque o webhook é um aviso, e não a fonte da verdade.
- Ao enviar: fila de saída na mesma transação, tentativas espaçadas, registro e reenvio.
- 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.