Webhooks

Verificar assinatura

Cada webhook é assinado com HMAC-SHA256, no mesmo esquema do Stripe. Sempre valide a assinatura antes de processar o evento.

O header de assinatura

O header x-onnix-signature traz dois campos: o t (timestamp) e o v1 (a assinatura). A assinatura é o HMAC-SHA256 da string {t}.{corpo_bruto}, usando como segredo o webhook_secret da cobrança (ou o secret do webhook configurado no dashboard).

Como verificar

  • Extraia t e v1 do header.
  • Concatene {t}.{corpo_bruto} usando o corpo exatamente como recebido (bytes brutos).
  • Calcule o HMAC-SHA256 com seu segredo e compare com v1 em tempo constante.
  • Rejeite se o t estiver fora de uma janela (ex.: 5 min) para mitigar replay.

Use o corpo bruto

Frameworks que fazem parse automático do JSON quebram a verificação: a reserialização muda os bytes. Configure a rota para receber o raw body antes de validar.
Header
x-onnix-signature: t=1780515992,v1=3a7f4b...e9d2

# t  = timestamp (segundos Unix)
# v1 = HMAC-SHA256(secret, "{t}.{corpo_bruto}") em hex
Verificação
import crypto from "node:crypto";

function verifyOnnixWebhook(rawBody, header, secret, toleranceSec = 300) {
  // header: "t=1780515992,v1=3a7f...e9d2"
  const parts = Object.fromEntries(
    header.split(",").map((p) => p.split("=")),
  );
  const t = Number(parts.t);
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");

  // comparação em tempo constante
  const ok = crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(parts.v1),
  );
  // proteção contra replay
  const fresh = Math.abs(Date.now() / 1000 - t) <= toleranceSec;
  return ok && fresh;
}