Webhooks
Verificar assinatura
Todo webhook é assinado com HMAC-SHA256 usando o secret da sua conta (
whsec_...). Verifique a assinatura antes de confiar no conteúdo — assim você garante que o evento veio da Reload Bank e não foi alterado.Formato do header
X-ReloadBank-Signature: t=1792161131,v1=5f2b7c1e9a0d...c2bt— timestamp Unix (segundos) do momento do envio.v1—HMAC_SHA256(secret, "<t>.<corpo bruto>")em hexadecimal minúsculo.
Passo a passo
- Leia o corpo da requisição bruto, exatamente como recebido (antes de qualquer parse JSON).
- Extraia
tev1do headerX-ReloadBank-Signature. - Rejeite se
testiver a mais de 5 minutos do horário atual (proteção contra replay). - Calcule o HMAC-SHA256 de
`${t}.${rawBody}`com o seu secret e compare comv1usando uma comparação em tempo constante.
Use o corpo bruto
Re-serializar o JSON (por exemplo comJSON.stringify(req.body)) altera espaços e ordem de campos e faz a assinatura não bater.Exemplos
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.RELOAD_WEBHOOK_SECRET; // whsec_...
const TOLERANCE_SECONDS = 5 * 60;
function verifySignature(rawBody, header, secret) {
const parts = Object.fromEntries(
String(header ?? "").split(",").map((p) => p.trim().split("=")),
);
const t = Number(parts.t);
if (!Number.isInteger(t) || !parts.v1) return false;
// Rejeita eventos antigos (proteção contra replay)
if (Math.abs(Math.floor(Date.now() / 1000) - t) > TOLERANCE_SECONDS) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(parts.v1, "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Use o corpo BRUTO: não deixe o express.json() reformatar o JSON antes
app.post("/webhooks/reloadbank", express.raw({ type: "application/json" }), (req, res) => {
const rawBody = req.body.toString("utf8");
if (!verifySignature(rawBody, req.get("X-ReloadBank-Signature"), SECRET)) {
return res.status(400).send("assinatura inválida");
}
const { event, data } = JSON.parse(rawBody);
// Processe de forma idempotente (ex.: pelo X-ReloadBank-Delivery ou data.id + event)
console.log(event, data.id, data.status);
res.sendStatus(200);
});
app.listen(3000);Rotação do secret
Se o secret da conta for alterado, as entregas seguintes (incluindo retentativas pendentes) passam a ser assinadas com o novo valor. Atualize seu servidor antes de rotacionar.