Receber webhooks

Receba eventos em tempo real e valide a assinatura HMAC SHA-256 antes de processar.

Como funciona

Quando um evento relevante acontece, a Sinnu envia um POST para a URL configurada no painel, em Integrações. O corpo é um envelope JSON com version, type e data.

Eventos

O campo type identifica o que aconteceu. Trate eventos desconhecidos de forma tolerante para manter compatibilidade com novos tipos.

EventoQuando é disparado
transaction.approvedPagamento confirmado.
transaction.declinedPagamento recusado.
transaction.refundedTransação estornada.
transaction.canceledTransação cancelada.
transaction.changedDados ou status da transação mudaram.
transaction.chargebackChargeback ou MED registrado.
withdrawal.approvedSaque aprovado.
Exemplo de payload
{
  "version": "1",
  "type": "transaction.approved",
  "data": {
    "id": "3b93f1b1-2d89-4b4d-95f9-7a2fd9b81234",
    "status": "paid",
    "amount": 12990,
    "currency": "BRL",
    "installments": 1,
    "paymentMethod": "pix",
    "createdAt": "2026-06-12T12:00:00.000Z",
    "card": null,
    "customer": {
      "name": "Maria Souza",
      "email": "maria@exemplo.com",
      "document": "123.***.***-09"
    }
  }
}

Validando a assinatura

Cada entrega inclui o header X-Sinnu-Signature no formato sha256=<hex>. Calcule o HMAC SHA-256 sobre o corpo bruto da requisição usando o segredo do webhook e compare em tempo constante.

Com o SDK (@sinnu/sdk)

O SDK valida a assinatura com constructEvent e devolve o evento desserializado.

Verificação com o SDK (Express)
import express from "express";
import { Sinnu, SinnuWebhookSignatureError } from "@sinnu/sdk";

const sinnu = new Sinnu({ apiKey: process.env.SINNU_API_KEY! });
const app = express();

app.post(
  "/webhooks/sinnu",
  express.raw({ type: "application/json" }),
  (req, res) => {
    try {
      const event = sinnu.webhooks.constructEvent({
        payload: req.body,
        signature: req.header("X-Sinnu-Signature"),
        secret: process.env.SINNU_WEBHOOK_SECRET!,
      });

      if (event.type === "transaction.approved") {
        console.log("pagamento confirmado:", event.data.id);
      }

      return res.sendStatus(200);
    } catch (err) {
      if (err instanceof SinnuWebhookSignatureError) {
        return res.sendStatus(400);
      }
      throw err;
    }
  },
);

app.listen(3000);

Sem o SDK

Se preferir não usar o SDK, calcule o HMAC manualmente com node:crypto:

Validação manual em Node.js (Express)
import crypto from "node:crypto";
import express from "express";

const app = express();
const WEBHOOK_SECRET = process.env.SINNU_WEBHOOK_SECRET;

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

app.post(
  "/webhooks/sinnu",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const assinatura = extrairAssinatura(req.header("X-Sinnu-Signature") ?? "");
    const esperada = crypto
      .createHmac("sha256", WEBHOOK_SECRET)
      .update(req.body)
      .digest("hex");

    const a = Buffer.from(assinatura, "hex");
    const b = Buffer.from(esperada, "hex");
    const ok = a.length === b.length && crypto.timingSafeEqual(a, b);

    if (!ok) {
      return res.status(400).send("assinatura inválida");
    }

    const event = JSON.parse(req.body.toString("utf8"));
    console.log("evento sinnu:", event.type, event.data.id);

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

app.listen(3000);

Reentregas e retries

Se seu endpoint responder com erro ou demorar demais, a Sinnu tenta reenviar o evento. Por isso:

  • Idempotência: use type, data.id edata.status como chave de processamento.
  • Resposta rápida: retorne 200 depois de validar e persistir o recebimento; processe o trabalho pesado em fila.