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...c2b
  • t — 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 t e v1 do header X-ReloadBank-Signature.
  • Rejeite se t estiver 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 com v1 usando uma comparação em tempo constante.

Use o corpo bruto

Re-serializar o JSON (por exemplo com JSON.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.