Pular para o conteúdo
Docs

Solução de problemas

Por que a assinatura do meu webhook não confere?

Resolva falhas na verificação de assinatura de webhooks. Assine o corpo bruto com o segredo whsec_ completo e o valor de X-Emailit-Timestamp.

Atualizado em 1 de out. de 2026

Este artigo ajuda quando o seu endpoint rejeita os webhooks do Emailit porque a assinatura que você calcula não é igual ao cabeçalho X-Emailit-Signature. Em quase todos os casos, os bytes que você assina ou o segredo que você usa são diferentes dos que o Emailit usou.

Sintomas

  • O seu handler retorna 401 ou 400 com “invalid signature” para todos os webhooks, incluindo os eventos de Send test do painel.
  • As requisições na aba Requests do webhook mostram tentativas com falha, com o código de status de erro do seu endpoint.
  • A verificação funciona em um teste unitário com um payload feito à mão, mas falha nas entregas reais.

Causa

O Emailit assina cada requisição assim:

Text
signature = hex( HMAC-SHA256( secret, "<X-Emailit-Timestamp>.<raw request body>" ) )

A assinatura falha quando uma das três entradas é diferente:

  • O corpo foi serializado de novo. Os frameworks muitas vezes interpretam o JSON antes de o seu handler rodar. Chamar JSON.stringify(req.body) produz espaços e uma ordem de chaves diferentes dos bytes que o Emailit enviou. O corpo é um array JSON de até 100 eventos, então um código que espera um objeto também pode alterar o formato dele.
  • O segredo está errado. Use o valor completo, incluindo o prefixo whsec_. Cada webhook tem o próprio segredo, e redefini-lo invalida o antigo na hora, inclusive para as novas tentativas pendentes.
  • O timestamp está ausente ou é diferente. Use o valor exato do cabeçalho X-Emailit-Timestamp e junte-o ao corpo com um único ponto.
  • A comparação está errada. O cabeçalho está em hexadecimal minúsculo, não em Base64, e não tem o prefixo sha256=.
  • Algo na frente da sua aplicação alterou o corpo, como um proxy que o recodifica ou o descompacta.

Solução

  1. Leia o corpo bruto. Capture os bytes antes que qualquer parser JSON seja executado. No Express, monte express.raw() apenas na rota do webhook:

    webhook.js
    import crypto from 'node:crypto';
    
    app.post('/webhooks/emailit', express.raw({ type: 'application/json' }), (req, res) => {
      const timestamp = req.get('X-Emailit-Timestamp');
      const expected = crypto
        .createHmac('sha256', process.env.EMAILIT_WEBHOOK_SECRET) // whsec_...
        .update(`${timestamp}.${req.body.toString('utf8')}`)
        .digest('hex');
      const received = req.get('X-Emailit-Signature') ?? '';
      const valid = received.length === expected.length &&
        crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
      if (!valid) return res.sendStatus(401);
    
      const events = JSON.parse(req.body); // an array of events
      res.sendStatus(200);
    });

    Nos route handlers do Next.js, use await request.text(). No Laravel, use $request->getContent(). No Flask, use request.get_data().

  2. Obtenha o segredo correto. Chame Obter um webhook, que retorna o secret atual. No painel, o segredo é mostrado uma única vez, então, se você o perdeu, abra o webhook, escolha Webhook secret para redefini-lo e atualize a sua variável de ambiente imediatamente.

  3. Envie um evento de teste. Na página do webhook, escolha Send test e selecione um tipo de evento. O teste é assinado com o mesmo segredo, e a caixa de diálogo mostra o código de status e o corpo da resposta do seu endpoint.

  4. Confira a tolerância do seu relógio. Se você rejeitar timestamps antigos para evitar ataques de replay, permita alguns minutos de diferença e mantenha o relógio do servidor sincronizado. Cada tentativa de entrega, incluindo as novas tentativas, traz um timestamp novo.

  5. Tente de novo o que falhou. Quando a verificação funcionar, escolha Retry failed para reenviar as entregas com falha dos últimos 7 dias.

Para código em outras linguagens, consulte Assinatura das requisições de webhook. Para o formato do corpo, consulte Requisições de webhook.

Ainda com problemas?

Fale com o suporte ou pergunte no Discord. Informe o ID do webhook (wh_…) e o ID de um evento que falhou, nunca o segredo.

Esta página foi útil?

Obrigado pelo feedback.

Obrigado, lemos todas as mensagens.