Webhooks robustos: assinatura, idempotência e retries sem caos
Padrões para ingestão confiável de webhooks em integrações com Stripe, GitHub e parceiros críticos.
Resumo executivo
Padrões para ingestão confiável de webhooks em integrações com Stripe, GitHub e parceiros críticos.
Ultima atualizacao: 14/02/2026
Introdução: A falha silenciosa da integração orientada a eventos
Webhooks são a espinha dorsal da integração moderna entre sistemas SaaS. O Stripe envia confirmações de pagamento. O GitHub notifica pipelines de CI/CD. O Shopify dispara o fulfillment de pedidos. No entanto, webhooks são também um dos pontos de maior risco de falha silenciosa em sistemas em produção.
O perigo é que a entrega de webhooks parece simples — é apenas um POST HTTP — mas opera sob condições fundamentalmente não confiáveis:
- O sender vai retentar. O Stripe retenta entregas falhas até 16 vezes ao longo de 72 horas. Se seu handler não for idempotente, você processará o mesmo pagamento duas vezes.
- A rede vai mentir. Seu servidor pode processar o evento com sucesso, mas o sender nunca receber o
200 OK(por timeout ou conexão perdida). O sender retenta, e você processa novamente. - Atacantes vão forjar. Sem verificação de assinatura, qualquer pessoa que descubra sua URL de webhook pode enviar eventos falsos (ex: fabricar um evento
payment_succeededpara enviar produtos sem pagar).
A confiabilidade depende de três pilares inegociáveis: verificação forte de assinatura, ingestão assíncrona e execução idempotente.
Pilar 1: Verificação de assinatura
Todo provedor de webhook sério assina payloads com HMAC-SHA256 (ou similar). O provedor compartilha uma chave secreta com você; cada requisição inclui uma assinatura computada em um header.
Como funciona (exemplo Stripe)
typescriptimport Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
export async function handleWebhook(req: Request) {
const signature = req.headers['stripe-signature'];
const rawBody = await req.text(); // Deve ser o corpo bruto, não JSON parseado
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(rawBody, signature, process.env.STRIPE_WEBHOOK_SECRET);
} catch (err) {
// Assinatura inválida — rejeitar e logar para auditoria de segurança
return new Response('Assinatura inválida', { status: 401 });
}
// Assinatura válida — seguro para processar
await enqueueForProcessing(event);
return new Response('OK', { status: 200 });
}Regras críticas:
- Verifique antes de qualquer efeito colateral. Nunca toque no banco de dados, envie email ou dispare um workflow antes de validar a assinatura.
- Use o corpo bruto (raw body). Parsear o JSON e reserializá-lo pode mudar a formatação (ordenação de chaves, espaços), invalidando o HMAC.
- Valide timestamps. A maioria dos provedores inclui um timestamp. Rejeite eventos mais antigos que uma janela razoável (ex: 5 minutos) para prevenir ataques de replay.
Pilar 2: Ingestão assíncrona
O erro arquitetural mais comum é processar o webhook sincronamente dentro do handler HTTP. Isso falha porque:
- Risco de timeout: O Stripe dá 20 segundos para você retornar um
2xx. Processamento complexo (escritas no banco, chamadas a APIs externas, envio de emails) facilmente excede esse limite. - Amplificação de retries: Se ocorrer timeout, o sender retenta, e seu handler começa o trabalho _novamente_ — potencialmente criando efeitos colaterais duplicados ou transações sobrepostas.
O padrão correto: Aceitar-e-Enfileirar
POST /webhooks/stripe → Verificar assinatura → Persistir evento bruto → Retornar 200 → Fim (< 100ms)
↓
Worker da fila pega o evento
↓
Processar com guarda de idempotênciaO handler HTTP faz três coisas e nada mais:
- Verificar a assinatura.
- Persistir o envelope do evento bruto (incluindo headers, metadados de entrega e o corpo raw) em um banco de dados ou event store.
- Retornar
200 OKimediatamente.
Um worker em background (consumidor SQS, processador Bull queue, consumidor Kafka) então pega o evento e processa com garantias completas de idempotência.
Pilar 3: Execução idempotente
Como retries são garantidos, sua lógica de processamento de webhook deve ser idempotente: executar o mesmo evento múltiplas vezes deve produzir o mesmo resultado que executá-lo uma vez.
Padrão de implementação: Idempotência por event_id
Todo provedor importante inclui um identificador único do evento (event.id no Stripe, X-GitHub-Delivery no GitHub). Use como chave de deduplicação:
typescriptasync function processWebhookEvent(event: WebhookEvent) {
// 1. Verificar se já foi processado
const existing = await db.webhookEvents.findUnique({
where: { eventId: event.id },
});
if (existing?.status === 'completed') {
return; // Já processado — pular silenciosamente
}
if (existing?.status === 'processing') {
return; // Outro worker está tratando — pular para evitar race condition
}
// 2. Marcar como "processing" (com lock otimista)
await db.webhookEvents.upsert({
where: { eventId: event.id },
create: { eventId: event.id, status: 'processing', receivedAt: new Date() },
update: { status: 'processing' },
});
// 3. Executar lógica de negócio
try {
await handlePaymentSucceeded(event.data);
await db.webhookEvents.update({
where: { eventId: event.id },
data: { status: 'completed', processedAt: new Date() },
});
} catch (error) {
await db.webhookEvents.update({
where: { eventId: event.id },
data: { status: 'failed', error: error.message },
});
throw error; // Re-throw para que a fila retente
}
}Estados chave: pending → processing → completed / failed. O estado processing previne race conditions quando dois retries chegam simultaneamente.
Aprofundando a análise: Cenários reais de falha
| Cenário | Sem tratamento adequado | Com os três pilares |
|---|---|---|
**Stripe retenta um evento payment_intent.succeeded 3 vezes** | Cliente é cobrado uma vez mas produtos são enviados 3 vezes, ou 3 registros são criados no banco. | Check de idempotência no event.id garante que o envio dispara uma única vez. |
| Atacante descobre URL do webhook | Eventos falsos invoice.paid marcam faturas não pagas como pagas. | Verificação de assinatura HMAC rejeita todo payload forjado. |
| Handler demora 25 segundos para processar | Stripe dá timeout, retenta, handler agora tem duas execuções concorrentes. | Handler retorna 200 em <100ms. Worker da fila processa assincronamente. |
| Rede derruba a resposta 200 | Stripe nunca vê o acknowledgment, retenta. Handler processa novamente. | Guarda de idempotência detecta status completed, pula reprocessamento. |
Quando a confiabilidade de webhooks acelera a entrega
Tratar webhooks como contratos de integração de primeira classe previne a classe mais cara de bugs: corrupção silenciosa de dados em sistemas financeiros e transacionais.
Perguntas de decisão para o seu contexto de engenharia:
- Quais operações com efeitos colaterais exigem chaves de idempotência obrigatórias?
- Como você diferencia "em processamento" de "concluído" sob retries concorrentes?
- Qual janela de retenção de eventos brutos cobre atrasos realistas de rede e janelas de SLA de parceiros?
Roteiro de otimização contínua
- Valide assinatura HMAC e timestamp no ingresso. Rejeite eventos inválidos ou replayados antes de qualquer processamento.
- **Retorne
2xxrápido e delegue para workers da fila.** O handler HTTP deve completar em menos de 100ms. - **Imponha idempotência pelo
event_iddo provedor.** Use um modelo de três estados (pending/processing/completed) com locking otimista. - Execute reconciliação periódica. Busque eventos recentes da API do provedor e compare com seus eventos processados para capturar qualquer coisa que tenha sido perdida.
- Instrumente métricas de retry, deduplicação e falha terminal. Monitore com que frequência eventos são retentados, deduplicados e falham permanentemente.
- Teste cenários de duplicidade e reordenação em staging. Simule os exatos modos de falha (entrega duplicada, eventos fora de ordem, retries atrasados) que a produção encontrará.
Como validar evolução em produção
Meça a confiabilidade de webhooks monitorando:
- Efeitos colaterais duplicados prevenidos: Quantos pagamentos, envios ou notificações duplicados foram bloqueados por guardas de idempotência?
- Esforço de reconciliação manual: Quanto tempo o time gasta manualmente corrigindo eventos perdidos ou processados em duplicidade?
- Incidentes financeiros por retries não-idempotentes: Quantos incidentes com impacto direto na receita foram causados por processamento duplicado?
Quer transformar esse plano em execução com previsibilidade técnica e impacto no negócio? Falar com especialista em web com a Imperialis para desenhar, implementar e operar essa evolução.