Webhooks
Verificando a assinatura
Toda entrega de webhook vem assinada. Verificar a assinatura garante que o evento partiu da Verdin e não foi adulterado no caminho — nunca confie num webhook sem checar.
O cabeçalho verdin-signature
Cada requisição traz o cabeçalho verdin-signature com dois campos: t (timestamp Unix da assinatura) e v1 (o HMAC-SHA256 em hexadecimal):
Cabeçalho
verdin-signature: t=1751630711,v1=3a5f...c9d2O v1 é o HMAC-SHA256 da string <t>.<body> (o timestamp, um ponto, e o corpo bruto da requisição), usando o secret do endpoint como chave.
Use o corpo BRUTO
A assinatura cobre os bytes exatos recebidos. Se você reserializar o JSON (parse + stringify) antes de verificar, a assinatura não vai bater. Capture o corpo cru — em Express, com
express.raw().Verificando em Node.js
Recalcule o HMAC com o seu secret e compare com o v1 recebido usando timingSafeEqual (comparação em tempo constante):
verifyVerdinSignature.js
import { createHmac, timingSafeEqual } from "node:crypto";
/**
* Verifica a assinatura de um webhook Verdin.
* @param rawBody corpo BRUTO da requisição (string ou Buffer) — sem reparse
* @param header valor do cabeçalho "verdin-signature"
* @param secret secret do endpoint (mostrado no painel)
* @param toleranceSec janela de tolerância do timestamp (padrão 5 min)
*/
export function verifyVerdinSignature(
rawBody,
header,
secret,
toleranceSec = 300,
) {
// 1. separe "t=<unix>,v1=<hmac>"
const parts = Object.fromEntries(
header.split(",").map((kv) => kv.split("=").map((s) => s.trim())),
);
const timestamp = Number(parts.t);
const signature = parts.v1;
if (!Number.isFinite(timestamp) || !signature) return false;
// 2. (opcional) rejeite eventos muito antigos — protege contra replay
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > toleranceSec) return false;
// 3. recalcule o HMAC de "<t>.<body>" com o secret do endpoint
const body = typeof rawBody === "string" ? rawBody : rawBody.toString("utf8");
const expected = createHmac("sha256", secret)
.update(`${timestamp}.${body}`)
.digest("hex");
// 4. compare em tempo constante (evita timing attack)
const a = Buffer.from(expected, "hex");
const b = Buffer.from(signature, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}Usando no seu handler
Handler Express
app.post(
"/webhooks/verdin",
express.raw({ type: "application/json" }),
(req, res) => {
const ok = verifyVerdinSignature(
req.body, // Buffer bruto
req.header("verdin-signature") ?? "",
process.env.VERDIN_WEBHOOK_SECRET,
);
if (!ok) return res.sendStatus(400); // assinatura inválida
const evt = JSON.parse(req.body.toString("utf8"));
// ... processe evt de forma idempotente
res.sendStatus(200);
},
);Checklist de segurança
- Compare a assinatura com
timingSafeEqual, nunca com===. - Rejeite eventos com timestamp fora de uma janela de tolerância (ex.: 5 minutos) para mitigar replay.
- Guarde o
secretem variável de ambiente; rotacione se suspeitar de vazamento. - Só então confie no
data— e processe de forma idempotente.