Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

Webhooks Seguros com Node.js

Atualizado em: 27 de julho de 2026

Servidor protegido recebendo webhooks seguros com Node.js

Webhooks são uma forma eficiente de integrar sistemas sem depender de consultas repetidas. Em vez de uma aplicação perguntar a cada minuto se um pagamento foi aprovado, o provedor envia uma requisição HTTP assim que o evento acontece. Esse modelo reduz latência e consumo de recursos, mas também cria desafios importantes de segurança, confiabilidade e observabilidade.

Neste guia, você vai aprender a implementar webhooks seguros com Node.js, validar assinaturas HMAC, evitar processamento duplicado, responder rapidamente, organizar retries e usar filas para tarefas demoradas. O objetivo é construir um endpoint que funcione bem em desenvolvimento e continue previsível quando receber eventos reais em produção.

O que é um webhook?

Um webhook é uma requisição HTTP enviada automaticamente por um sistema para outro quando um evento ocorre. Plataformas de pagamento, serviços de e-mail, repositórios de código, ferramentas de automação e sistemas de entrega usam esse padrão para informar mudanças em tempo quase real.

Normalmente, o provedor envia uma requisição POST com um corpo JSON. O receptor valida a origem, interpreta o tipo de evento e executa uma ação. Se você ainda está revisando os fundamentos, consulte o artigo sobre o que é Node.js e o guia de como criar uma API com Node.js.

Principais riscos

Um endpoint público pode receber requisições falsas, repetidas ou malformadas. Os riscos mais comuns são:

  • aceitar eventos enviados por um atacante;
  • processar o mesmo evento várias vezes;
  • confiar em campos sem validação;
  • demorar demais para responder e provocar retries;
  • registrar segredos ou dados sensíveis nos logs;
  • executar ações irreversíveis antes de confirmar a autenticidade.

A segurança de um webhook não depende apenas de HTTPS. O TLS protege o transporte, mas o servidor ainda precisa verificar se a mensagem realmente veio do provedor esperado.

Preparando o projeto

Crie um projeto simples com Express:

mkdir webhooks-node
cd webhooks-node
npm init -y
npm install express

No package.json, adicione "type": "module". Depois, crie src/server.js:

import express from "express";

const app = express();
app.use(express.json());

app.post("/webhooks/provider", (req, res) => {
  console.log(req.body);
  res.sendStatus(204);
});

app.listen(3000, () => {
  console.log("Servidor em http://localhost:3000");
});

Esse exemplo recebe eventos, mas ainda não é seguro. O próximo passo é preservar o corpo original para validar a assinatura.

Por que o corpo bruto é importante?

Muitos provedores calculam a assinatura sobre os bytes exatos enviados. Se o Express converter o JSON em objeto e depois você serializar novamente, espaços, ordem de campos ou codificação podem mudar. Por isso, a validação deve usar o corpo bruto.

app.use(express.json({
  verify: (req, res, buffer) => {
    req.rawBody = buffer;
  }
}));

Agora req.rawBody contém os bytes usados na assinatura. Leia a documentação do provedor, porque nomes de cabeçalhos, formato e algoritmo variam.

Validando assinatura HMAC

Uma estratégia comum usa HMAC com SHA-256. O provedor combina o corpo da requisição com um segredo compartilhado e envia o resultado em um cabeçalho. O servidor repete o cálculo e compara os valores.

import crypto from "node:crypto";

function verifySignature(rawBody, signature, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");

  const expectedBuffer = Buffer.from(expected, "utf8");
  const receivedBuffer = Buffer.from(signature ?? "", "utf8");

  if (expectedBuffer.length !== receivedBuffer.length) {
    return false;
  }

  return crypto.timingSafeEqual(
    expectedBuffer,
    receivedBuffer
  );
}

Use timingSafeEqual para reduzir diferenças de tempo durante a comparação. Guarde o segredo em variável de ambiente e nunca o inclua no repositório.

app.post("/webhooks/provider", (req, res) => {
  const signature = req.get("x-webhook-signature");
  const secret = process.env.WEBHOOK_SECRET;

  if (!secret || !verifySignature(req.rawBody, signature, secret)) {
    return res.status(401).json({ message: "Assinatura inválida" });
  }

  res.sendStatus(204);
});

A documentação de Crypto no Node.js explica HMAC e comparações seguras. A documentação oficial do Express também detalha middlewares e tratamento de requisições.

Protegendo contra replay attacks

Mesmo uma mensagem legítima pode ser capturada e reenviada. Para reduzir esse risco, provedores costumam incluir um timestamp. O servidor deve rejeitar mensagens muito antigas.

function isRecent(timestamp, toleranceSeconds = 300) {
  const eventTime = Number(timestamp);
  const now = Math.floor(Date.now() / 1000);

  return Number.isFinite(eventTime)
    && Math.abs(now - eventTime) <= toleranceSeconds;
}

Quando o timestamp faz parte do cálculo da assinatura, um atacante não consegue alterá-lo sem invalidar o HMAC. Use uma tolerância curta, considerando possíveis diferenças de relógio.

Idempotência

Provedores repetem webhooks quando não recebem resposta ou quando há falha de rede. Portanto, o mesmo evento pode chegar mais de uma vez. Seu processamento deve ser idempotente: executar duas vezes deve produzir o mesmo resultado que executar uma vez.

Use um identificador único do evento e registre-o antes de aplicar ações irreversíveis:

async function processEvent(event) {
  const exists = await eventRepository.exists(event.id);

  if (exists) {
    return { duplicated: true };
  }

  await eventRepository.create({
    id: event.id,
    type: event.type,
    status: "processing"
  });

  try {
    await handleEvent(event);
    await eventRepository.markCompleted(event.id);
  } catch (error) {
    await eventRepository.markFailed(event.id);
    throw error;
  }
}

Crie uma constraint única no banco para o ID do evento. Assim, duas instâncias concorrentes não processam o mesmo webhook simultaneamente.

Responda rapidamente

O endpoint não deve executar tarefas demoradas antes de responder. Enviar e-mail, gerar relatório ou chamar várias APIs pode ultrapassar o timeout do provedor. O padrão mais seguro é validar, armazenar e colocar o evento em uma fila.

app.post("/webhooks/provider", async (req, res) => {
  if (!verifyProviderRequest(req)) {
    return res.sendStatus(401);
  }

  await queue.add("provider-event", {
    eventId: req.body.id,
    payload: req.body
  });

  return res.sendStatus(202);
});

Um worker separado processa a tarefa. Se você quiser revisar filas e armazenamento temporário, leia o guia sobre Redis com Node.js para cache e filas.

Validando o payload

Assinatura válida não significa payload correto. Verifique campos obrigatórios, tipos e valores permitidos antes de usar os dados.

function validateEvent(payload) {
  if (!payload || typeof payload !== "object") {
    return false;
  }

  if (typeof payload.id !== "string") {
    return false;
  }

  if (![
    "payment.approved",
    "payment.failed"
  ].includes(payload.type)) {
    return false;
  }

  return true;
}

Bibliotecas de schema podem deixar a validação mais clara. O importante é nunca confiar em valores recebidos apenas porque vieram de um provedor conhecido.

Retries e dead letter queue

Falhas temporárias devem ser repetidas com atraso crescente. Uma política comum usa backoff exponencial. Depois de um número limitado de tentativas, envie a tarefa para uma fila de erros, conhecida como dead letter queue.

  • defina máximo de tentativas;
  • registre motivo e horário da falha;
  • não repita indefinidamente erros de validação;
  • permita reprocessamento manual;
  • monitore crescimento da fila de erros.

Retentativas sem controle podem amplificar uma indisponibilidade. Diferencie falhas temporárias, como timeout, de falhas permanentes, como evento inválido.

Logs e observabilidade

Registre o ID do evento, tipo, duração, resultado e número da tentativa. Evite armazenar segredo, assinatura completa, tokens, dados de cartão ou informações pessoais desnecessárias.

logger.info({
  eventId: event.id,
  type: event.type,
  attempt,
  durationMs,
  result: "completed"
}, "Webhook processado");

Crie métricas para eventos recebidos, rejeitados, duplicados, concluídos e falhos. Alertas devem detectar aumento de assinaturas inválidas ou filas acumuladas. O artigo sobre OpenTelemetry no Node.js ajuda a organizar traces e métricas.

Testando localmente

Durante o desenvolvimento, use uma ferramenta de túnel para expor temporariamente o servidor local. Configure um segredo exclusivo de teste e envie eventos fictícios. Teste pelo menos estes cenários:

  • assinatura correta;
  • assinatura incorreta;
  • timestamp expirado;
  • evento duplicado;
  • payload incompleto;
  • falha temporária no worker;
  • concorrência com o mesmo ID.

Não reutilize segredos de produção no ambiente local. Também confirme que o endpoint responde dentro do tempo esperado.

Boas práticas para produção

  • Use HTTPS: nunca aceite eventos sensíveis por HTTP aberto.
  • Rotacione segredos: planeje uma janela em que dois segredos possam ser aceitos.
  • Restrinja tamanho: limite o corpo para evitar consumo excessivo de memória.
  • Use idempotência: toda ação financeira ou irreversível precisa tolerar duplicatas.
  • Separe recebimento e processamento: use uma fila para tarefas demoradas.
  • Monitore falhas: eventos perdidos precisam gerar alerta.
  • Documente versões: trate mudanças de schema de forma explícita.
  • Proteja logs: aplique retenção e controle de acesso.

Conclusão

Criar webhooks seguros com Node.js exige mais do que abrir uma rota POST. O endpoint precisa verificar assinatura, limitar replay, validar o payload, impedir duplicidade e responder rapidamente. A execução pesada deve ocorrer em uma fila, com retries controlados e visibilidade sobre falhas.

Comece implementando o corpo bruto e a verificação HMAC. Depois, adicione idempotência no banco, fila, métricas e testes de concorrência. Com essas camadas, os webhooks deixam de ser uma integração frágil e passam a funcionar como uma parte confiável da arquitetura.

Os 10 Melhores Cursos de Programação de 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita