Pular para o conteúdo
Docs

Verificar a assinatura do webhook (HMAC)

Atualizado em

Nesta página

Toda entrega de webhook é assinada com o segredo do endpoint. Verifique a assinatura antes de confiar no conteúdo: sem isso, qualquer pessoa que descubra a sua URL consegue enviar eventos falsos.

Headers de cada entrega#

Header Exemplo Uso
X-Emitra-Event nfse.authorized Tipo do evento (o mesmo de type no corpo).
X-Emitra-Delivery f1e2d3c4-b5a6-4978-8a9b-0c1d2e3f4a5b ID desta entrega. Uma nova tentativa da mesma entrega mantém o ID.
X-Emitra-Timestamp 1762866933 Momento da assinatura, em segundos Unix.
X-Emitra-Signature sha256=5d1f... Assinatura HMAC-SHA256 em hexadecimal.
X-Emitra-Test true Presente só em eventos de chaves de teste (sandbox).

Como a assinatura é calculada#

Fórmula
assinatura = "sha256=" + hex( HMAC_SHA256( segredo, timestamp + "." + corpo_bruto ) )
  • segredo é o segredo do endpoint, gerado pela Emitra e mostrado quando você cria o webhook.
  • timestamp é o valor exato do header X-Emitra-Timestamp.
  • corpo_bruto são os bytes do corpo da requisição como chegaram, antes de qualquer parse de JSON.

Passo a passo#

  1. Leia os headers X-Emitra-Timestamp e X-Emitra-Signature. Se faltar algum, responda 400.
  2. Recuse timestamps fora de uma janela de tolerância. Recomendamos 5 minutos para os dois lados, o que bloqueia a reutilização de uma entrega capturada.
  3. Calcule o HMAC-SHA256 de timestamp + "." + corpo_bruto com o seu segredo.
  4. Compare com o valor do header usando uma comparação em tempo constante. Nunca use == em strings de assinatura.
  5. Só então faça o parse do JSON, responda 2xx e processe o evento.

Código#

Os exemplos leem o segredo de EMITRA_WEBHOOK_SECRET.

Node (Express)
import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.EMITRA_WEBHOOK_SECRET;
const TOLERANCE_SECONDS = 5 * 60;

function isValidSignature(rawBody, timestamp, signatureHeader) {
  if (!timestamp || !signatureHeader) return false;

  const ts = Number.parseInt(timestamp, 10);
  if (!Number.isFinite(ts)) return false;
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - ts) > TOLERANCE_SECONDS) return false;

  const expected =
    "sha256=" +
    crypto
      .createHmac("sha256", SECRET)
      .update(`${timestamp}.`)
      .update(rawBody)
      .digest("hex");

  const a = Buffer.from(expected, "utf8");
  const b = Buffer.from(signatureHeader, "utf8");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// express.raw mantém o corpo como Buffer, sem parse
app.post(
  "/webhooks/emitra",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const ok = isValidSignature(
      req.body,
      req.get("X-Emitra-Timestamp"),
      req.get("X-Emitra-Signature"),
    );
    if (!ok) return res.status(400).send("assinatura inválida");

    const event = JSON.parse(req.body.toString("utf8"));
    // Enfileire o processamento e responda rápido
    queue.add(event, { jobId: req.get("X-Emitra-Delivery") });
    res.status(200).send("ok");
  },
);

app.listen(3000);

Teste a verificação#

Use o botão Testar do webhook no painel ou a rota POST /integracao/webhooks/{id}/testar (veja a referência). A entrega de teste é assinada com o mesmo segredo, então ela valida o seu código de ponta a ponta.

Se a assinatura falhar:

  • confira se você está lendo o corpo bruto, e não o JSON reserializado;
  • confira se o segredo é o do endpoint certo (cada endpoint tem o seu);
  • confira o relógio do servidor (NTP), por causa da janela de tolerância;
  • se você girou o segredo no painel, atualize a variável EMITRA_WEBHOOK_SECRET.

Próximos passos#

Esta página foi útil?