Conciliar pagamentos com webhooks

Construa um endpoint que recebe eventos da Sinnu, valida a assinatura e marca pedidos como pagos de forma confiável.

Webhooks são a forma recomendada de saber quando um pagamento foi confirmado. Neste tutorial você vai montar um endpoint idempotente usando o corpo bruto e a assinatura X-Sinnu-Signature.

Passo 1: exponha um endpoint

Crie uma rota HTTP pública que aceite POST e configure essa URL no painel Sinnu, em Integrações.

Passo 2: capture o corpo bruto

A validação HMAC depende do corpo exatamente como recebido. Em Express, use express.raw() nessa rota.

Passo 3: valide a assinatura

server.js
import crypto from "node:crypto";
import express from "express";

const app = express();
const WEBHOOK_SECRET = process.env.SINNU_WEBHOOK_SECRET;
const processados = new Set(); // troque por seu banco em produção

function extrairAssinatura(header = "") {
  return header.startsWith("sha256=") ? header.slice("sha256=".length) : header;
}

function assinaturaValida(rawBody, header) {
  const assinatura = extrairAssinatura(header);
  const esperada = crypto
    .createHmac("sha256", WEBHOOK_SECRET)
    .update(rawBody)
    .digest("hex");

  const a = Buffer.from(assinatura, "hex");
  const b = Buffer.from(esperada, "hex");

  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post(
  "/webhooks/sinnu",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const header = req.header("X-Sinnu-Signature") ?? "";
    if (!assinaturaValida(req.body, header)) {
      return res.status(400).send("assinatura inválida");
    }

    const evento = JSON.parse(req.body.toString("utf8"));
    const chave = [evento.type, evento.data.id, evento.data.status].join(":");

    if (processados.has(chave)) {
      return res.status(200).send("já processado");
    }

    if (evento.type === "transaction.approved") {
      console.log("Pagamento confirmado:", evento.data.id);
      // marque o pedido como pago no seu banco
    }

    processados.add(chave);
    return res.status(200).send("ok");
  },
);

app.listen(3000, () => console.log("ouvindo webhooks na porta 3000"));

Passo 4: garanta idempotência

O mesmo evento pode ser entregue mais de uma vez. Use uma chave derivada de type, data.id e data.status para evitar processamento duplicado.

Passo 5: teste de ponta a ponta

  1. Suba o servidor e exponha-o com um túnel, como ngrok.
  2. Configure a URL pública no painel Sinnu.
  3. Crie um link seguindo o tutorial Cobrar via Pix em 5 minutos.
  4. Realize o pagamento e confirme que o evento transaction.approved chegou e foi processado uma única vez.