Assinatura
Cada webhook tem um signing_secret (whsec_…), retornado uma única vez: na criação ou em POST /webhooks/{id}/rotate-secret. Com ele você comprova que a notificação saiu da SancoPay e não foi alterada no caminho.
X-Webhook-Signature: t=1757937600,v1=5a9e3da11436d754656c959c03cb8a72d432577255a0a1c8479f41e7faaaa181Como verificar
- Leia o header
X-Webhook-Signaturee separet(unix, em segundos) e o(s) valor(es)v1. - Monte a mensagem
<t>.<corpo bruto>: o corpo exatamente como chegou, antes do parse do JSON. - Calcule o HMAC-SHA256 da mensagem usando o
signing_secretinteiro (com o prefixowhsec_) como chave, em hexadecimal minúsculo. - Compare em tempo constante com cada
v1e aceite se algum bater. - Recuse se
testiver a mais de 5 minutos do horário atual (proteção contra reenvio de uma notificação capturada).
const crypto = require("crypto");const express = require("express"); const app = express();const SECRET = process.env.SANCOPAY_WEBHOOK_SECRET; // whsec_... // Use o corpo BRUTO: fazer parse e re-serializar o JSON muda os bytes.app.post("/webhooks/sancopay", express.raw({ type: "application/json" }), (req, res) => { const header = req.get("X-Webhook-Signature") || ""; let t = 0; const v1 = []; for (const part of header.split(",")) { const [key, value] = part.trim().split("="); if (key === "t") t = Number(value); if (key === "v1") v1.push(value); } const now = Math.floor(Date.now() / 1000); if (!t || Math.abs(now - t) > 300) return res.sendStatus(401); const expected = crypto .createHmac("sha256", SECRET) .update(t + ".") .update(req.body) .digest("hex"); const valid = v1.some( (sig) => sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)) ); if (!valid) return res.sendStatus(401); const { event, data } = JSON.parse(req.body); // ... processe o evento (ex.: charge.paid) res.sendStatus(200);});