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") {
      const { id, link, customer, salesperson } = evento.data;
      console.log("Pagamento confirmado:", id, link?.title, customer.phone, salesperson?.name);
      // 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: leia os dados da venda

O transaction.approved já traz o produto, o comprador e o vendedor responsável. O telefone do comprador vem no payload, sem chamada extra.

data de um transaction.approved (fictício)
{
  "id": "3b93f1b1-2d89-4b4d-95f9-7a2fd9b81234",
  "status": "paid",
  "amount": 129.9,
  "paymentMethod": "pix",
  "customer": {
    "name": "Maria Souza",
    "email": "maria@exemplo.com",
    "document": "12**********09",
    "phone": "11999998888"
  },
  "link": { "id": "8f0c2a4e-6b1d-4c3a-9e57-0d2b7c1a9f30", "title": "Curso de Planilhas" },
  "items": [
    { "title": "Curso de Planilhas", "quantity": 1, "unitPrice": 129.9, "externalRef": null }
  ],
  "salesperson": { "id": 42, "name": "João Lima", "email": "joao@exemplo.com" }
}
CampoUse para
link.titleSaber qual produto foi comprado. É null quando a cobrança foi criada pela API.
itemsItens, quantidades e o seu código de produto em externalRef.
customer.phoneAchar o comprador no CRM ou no WhatsApp.
salespersonAtribuir a venda ao vendedor escolhido no link. É null quando o link não tem um.

Passo 5: 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 6: 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.