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.
| Evento | Quando é disparado |
|---|---|
transaction.approved | Pagamento confirmado. |
transaction.declined | Pagamento recusado. |
transaction.refunded | Transação estornada. |
transaction.canceled | Transação cancelada. |
transaction.changed | Dados ou status da transação mudaram. |
transaction.chargeback | Chargeback ou MED registrado. |
withdrawal.approved | Saque aprovado. |
{
"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"
}
}
}Payload público
O webhook público expõe apenas campos permitidos para integração. Dados sensíveis de cartão, antifraude, adquirente e documento cru não são enviados.
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.
Use o corpo bruto
Calcule o HMAC sobre o corpo exato recebido, antes de qualquer parsing ou reserialização de JSON. Mudanças de espaços ou ordem de chaves invalidam a assinatura.
Com o SDK (@sinnu/sdk)
O SDK valida a assinatura com constructEvent e devolve o evento desserializado.
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:
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.idedata.statuscomo chave de processamento. - Resposta rápida: retorne
200depois de validar e persistir o recebimento; processe o trabalho pesado em fila.
Teste localmente
Use um túnel, como ngrok, para expor seu endpoint local e configure a URL no painel. O tutorial Conciliar pagamentos com webhooks mostra um fluxo completo.