A campainha em vez do 'já chegou?'
Você não liga para a pizzaria a cada dois minutos perguntando se a pizza saiu — deixa o endereço e o entregador toca a campainha. O endereço é a URL do webhook; a campainha é o POST. E como todo mundo pode tocar uma campainha, você olha pelo olho mágico antes de abrir: essa é a assinatura.
O problema: perguntar até a resposta mudar
Polling é a solução ingênua para 'quero saber quando X acontecer': pergunte sempre. O custo cresce dos dois lados — você paga requests inúteis e rate limit; o provedor paga carga de consultas cuja resposta não mudou — e a notícia ainda chega atrasada pelo intervalo do loop.
Webhook resolve latência e desperdício num golpe, mas muda a natureza do seu sistema: agora existe um endpoint público seu que terceiros chamam, e tudo que vale para uma API pública (autenticidade, disponibilidade, idempotência) passa a valer para ele.
O contrato implícito do emissor sério: ele assina o corpo (HMAC com segredo compartilhado), reenvia com backoff quando você não responde 2xx a tempo, e desiste depois de N tentativas — indo parar numa fila de mortos que alguém precisa olhar. Entrega é ao-menos-uma-vez e sem garantia de ordem: 'pagamento.confirmado' pode chegar antes de 'pagamento.criado'.
Por isso os quatro deveres do receptor: verificar a assinatura (o corpo sem ela é só uma história), deduplicar pelo id do evento, enfileirar e responder 200 imediatamente (processar inline estoura o timeout do emissor, que reenvia, que duplica), e tratar o webhook como notificação — a fonte da verdade é a API do provedor, consultada na conciliação periódica.
- Provedor (PSP)
- POST assinado (HMAC)Seu endpoint
- valida, deduplica, 200 jáFila interna
- processa no seu ritmoWorker
As três pontas da entrega
Camada 1/3Emissor
use ← →Camada 1
Emissor
assina, tenta de novo, desiste com registro
Do lado de quem envia: assinar o corpo com HMAC + timestamp (anti-replay), reenviar com backoff exponencial quando não vier 2xx, e ter uma fila de mortos com visibilidade — webhook que falhou para sempre é incidente do cliente, não log perdido.
Se violar: emissor sem retry transforma qualquer instabilidade do receptor em evento perdido para sempre — e sem assinatura, qualquer um forja um 'pagamento aprovado'.
Código
import { createHmac, timingSafeEqual } from "node:crypto";
// O contrato de um receptor de webhook decente:
// 1. verificar a assinatura 2. deduplicar 3. enfileirar 4. responder 200 JÁ
const processados = new Set<string>(); // Redis/tabela na vida real
const fila: unknown[] = []; // SQS/RabbitMQ na vida real
function verificarAssinatura(corpo: string, assinatura: string, segredo: string) {
const esperada = createHmac("sha256", segredo).update(corpo).digest("hex");
// comparação em tempo constante — timing attack não revela a assinatura
return (
esperada.length === assinatura.length &&
timingSafeEqual(Buffer.from(esperada), Buffer.from(assinatura))
);
}
async function receberWebhook(req: {
corpo: string;
cabecalhos: Record<string, string>;
}): Promise<{ status: number }> {
// 1. sem assinatura válida, o corpo é só uma história que alguém contou
if (!verificarAssinatura(req.corpo, req.cabecalhos["x-assinatura"], SEGREDO)) {
return { status: 401 };
}
const evento = JSON.parse(req.corpo) as { id: string; tipo: string };
// 2. entrega é ao-menos-uma-vez: o mesmo evento VAI chegar de novo
if (processados.has(evento.id)) return { status: 200 }; // duplicata: ok e tchau
// 3. + 4. guarda para processar depois e responde rápido —
// processar inline estoura o timeout do emissor, que reenvia, que duplica...
processados.add(evento.id);
fila.push(evento);
return { status: 200 };
}
const SEGREDO = process.env.WEBHOOK_SECRET ?? "";Casos de uso
Confirmação de pagamento PIX
Cenário
O checkout gera um QR Code e precisa liberar o pedido no instante em que o cliente paga — esperar um polling de 30s mata a experiência no caixa.
Como o padrão entra
O PSP chama seu webhook com 'pix.confirmado' assinado. O endpoint valida o HMAC, deduplica pelo id da transação, enfileira e responde 200; o worker libera o pedido e avisa o front por WebSocket. Uma conciliação horária consulta a API do PSP e pesca qualquer evento que a rede tenha engolido.
Trade-off: Você ganhou latência de segundos, mas agora opera um endpoint público de missão crítica — com monitoração, janela de manutenção coordenada e a conciliação como rede de segurança obrigatória.
CI disparado por push
Cenário
A cada push no repositório, a esteira de build precisa começar imediatamente — clonar o repo de minuto em minuto para 'ver se mudou' não escala para centenas de projetos.
Como o padrão entra
O GitHub chama o webhook do servidor de CI com o evento de push assinado. O CI valida, enfileira o build e responde — o pipeline começa dois segundos depois do git push, e o custo de polling de centenas de repositórios simplesmente desaparece.
Trade-off: Um webhook perdido = build que nunca rodou; por isso os servidores de CI sérios mantêm um resync periódico comparando o último commit conhecido com o real.
Armadilhas
Processar dentro do handler
O emissor dá 10 segundos e você gasta 30 processando: timeout, retry, e o mesmo evento processado duas vezes em paralelo. O handler síncrono valida, deduplica, enfileira e responde — o resto é trabalho do worker.
Confiar no corpo sem verificar a assinatura
Um endpoint público que aceita qualquer POST como verdade permite que qualquer um 'aprove' o próprio pagamento com um curl. A assinatura HMAC sobre o corpo cru é o que separa notificação de terceiro confiável de entrada de usuário anônimo.
Assumir entrega única e em ordem
Retry gera duplicata; rede gera desordem. Sem dedupe por id, o mesmo pagamento credita duas vezes; sem tolerância a desordem, o 'confirmado' que chegou antes do 'criado' quebra o fluxo. Os dois são o caso normal, não o excepcional.
Webhook como fonte da verdade
Webhook é notificação de melhor esforço: emissores perdem eventos, DLQs transbordam, endpoints ficam fora do ar. Quem decide estado de dinheiro é a consulta à API do provedor — a conciliação periódica não é paranoia, é o contrato.
Quando usar × quando evitar
Use quando
3- Notificação de eventos de terceiros com latência importante — pagamento, logística, mensageria.
- Substituir polling caro contra APIs com rate limit.
- Encadear sistemas seus via eventos HTTP quando uma fila compartilhada não é opção.
Evite quando
3- Receptores que não podem expor URL pública (apps móveis, desktop) — use push/polling.
- Fluxos que exigem garantia forte de entrega e ordem — fila com offset faz isso melhor.
- Como única fonte de estado financeiro — sem conciliação, cedo ou tarde diverge.
Testar o que ficou
4 armadilhas de Webhooks viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.
Onde isto aparece de verdade
Você já usa isto, talvez sem saber o nome.
webhooks do StripeEm vez de você ficar perguntando, o Stripe faz um POST no seu endpoint quando a cobrança muda de estado — a notificação chega sozinha.webhooks do GitHubUm push no repositório dispara uma chamada HTTP para o serviço de CI, que começa o build sem ninguém consultar o GitHub em loop.callback de gateway de pagamentoO provedor avisa a sua loja de forma assíncrona quando o Pix cai, fechando o pedido sem polling do lado de cá.
O padrão em uma linha
Sem nome de domínio, sem cerimônia. Só a ideia.
// Eles chamam você quando o fato acontece.
app.post("/hooks/stripe", verificarAssinatura, handler);O que ele cobra
1 salto a mais entre a chamada e o efeito.
- Você passa a manter um endpoint público, com verificação de assinatura e proteção contra reenvio
- A entrega é do provedor e pode falhar, repetir ou chegar fora de ordem — o consumidor precisa ser idempotente
Não vale se: você controla os dois lados no mesmo processo — aí uma chamada direta ou um evento interno dispensa a rede.
Onde isto aparece
Continue por aqui
- ObserverDefine uma dependência um-para-muitos: quando um objeto muda de estado, todos os seus dependentes são notificados automaticamente.
- IdempotênciaExecutar a mesma operação duas vezes tem o mesmo efeito de executar uma: é o que torna o retry seguro. Em rede, onde 'não sei se foi' é resposta comum, é a diferença entre reenviar e cobrar em dobro.
- Append-onlyUma estrutura onde a única escrita permitida é acrescentar no fim: nada se edita, nada se apaga. Corrigir é gravar um registro novo que supera o anterior — e é assim que funcionam o WAL do seu banco, o Kafka, o git e o seu extrato.