Knowledge

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.

14/02/20268 min de leituraKnowledge
Webhooks robustos: assinatura, idempotência e retries sem caos

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_succeeded para 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:

  1. 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.
  2. 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ência

O handler HTTP faz três coisas e nada mais:

  1. Verificar a assinatura.
  2. Persistir o envelope do evento bruto (incluindo headers, metadados de entrega e o corpo raw) em um banco de dados ou event store.
  3. Retornar 200 OK imediatamente.

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: pendingprocessingcompleted / failed. O estado processing previne race conditions quando dois retries chegam simultaneamente.

Aprofundando a análise: Cenários reais de falha

CenárioSem tratamento adequadoCom 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 webhookEventos falsos invoice.paid marcam faturas não pagas como pagas.Verificação de assinatura HMAC rejeita todo payload forjado.
Handler demora 25 segundos para processarStripe dá timeout, retenta, handler agora tem duas execuções concorrentes.Handler retorna 200 em <100ms. Worker da fila processa assincronamente.
Rede derruba a resposta 200Stripe 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

  1. Valide assinatura HMAC e timestamp no ingresso. Rejeite eventos inválidos ou replayados antes de qualquer processamento.
  2. **Retorne 2xx rápido e delegue para workers da fila.** O handler HTTP deve completar em menos de 100ms.
  3. **Imponha idempotência pelo event_id do provedor.** Use um modelo de três estados (pending/processing/completed) com locking otimista.
  4. 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.
  5. Instrumente métricas de retry, deduplicação e falha terminal. Monitore com que frequência eventos são retentados, deduplicados e falham permanentemente.
  6. 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.

Fontes

Leituras relacionadas