Ambiente
Base URLhttps://api.sancopay.com/api/v1
Arquivo para IA
Webhooks

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.

HeaderX-Webhook-Signature
X-Webhook-Signature: t=1757937600,v1=5a9e3da11436d754656c959c03cb8a72d432577255a0a1c8479f41e7faaaa181

Como verificar

  1. Leia o header X-Webhook-Signature e separe t (unix, em segundos) e o(s) valor(es) v1.
  2. Monte a mensagem <t>.<corpo bruto>: o corpo exatamente como chegou, antes do parse do JSON.
  3. Calcule o HMAC-SHA256 da mensagem usando o signing_secret inteiro (com o prefixo whsec_) como chave, em hexadecimal minúsculo.
  4. Compare em tempo constante com cada v1 e aceite se algum bater.
  5. Recuse se t estiver a mais de 5 minutos do horário atual (proteção contra reenvio de uma notificação capturada).
Exemplo
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);});