Webhooks
Recibí eventos firmados, verificá la firma y manejá reintentos.
Cauril te entrega un feed normalizado de eventos por webhook. Cada entrega es un POST con un Event en el body y dos headers clave: Cauril-Signature y Cauril-Event-Id.
Suscribirse
curl https://api.cauril.com/v1/webhooks/endpoints \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://tuapp.com/webhooks/cauril",
"enabled_events": ["payment.succeeded", "payment.failed", "refund.succeeded"]
}'La respuesta incluye secret una sola vez. Guardalo: lo necesitás para verificar la firma.
Verificar la firma
El header tiene la forma Cauril-Signature: t=<timestamp>,v1=<firma>, donde la firma es HMAC-SHA256 de la cadena "{t}.{cuerpo_crudo}" usando el secret del endpoint.
import express from "express";
import crypto from "node:crypto";
const app = express();
const SECRET = process.env.CAURIL_WEBHOOK_SECRET;
const TOLERANCE_SEC = 300;
// Importante: capturá el cuerpo crudo.
app.post("/webhooks/cauril", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("Cauril-Signature") ?? "";
const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
const t = Number(parts.t);
const raw = req.body.toString("utf8");
// Anti-replay: rechazá firmas viejas.
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > TOLERANCE_SEC) {
return res.sendStatus(400);
}
const expected = crypto.createHmac("sha256", SECRET).update(`${t}.${raw}`).digest("hex");
const ok =
parts.v1 &&
expected.length === parts.v1.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
if (!ok) return res.sendStatus(400);
const event = JSON.parse(raw);
// ... manejá el evento (idempotente por event.id) ...
res.sendStatus(200); // respondé 2xx rápido
});Respondé rápido
Devolvé un 2xx apenas recibís y validás el evento. Hacé el trabajo pesado de forma asíncrona: si tardás, contás como fallo y disparás reintentos.
Reintentos y durabilidad
Si tu endpoint no responde 2xx, Cauril reintenta con backoff exponencial y jitter: aproximadamente a los 0s, 1m, 5m, 30m, 2h, 6h, 12h, 24h… por hasta ~72 horas. Agotados los intentos, la entrega queda exhausted y podés reenviarla manualmente desde el dashboard.
2xx cuando puedas procesar el evento.Idempotencia del lado del receptor
Diseñá tu handler para tolerar entregas repetidas: ante un reintento, podés recibir el mismo evento más de una vez. Deduplicá por Cauril-Event-Id (o event.id) y procesá una sola vez.
Tipos de evento
| Evento | Cuándo |
|---|---|
payment.created | Se creó un pago (aún sin resolver). |
payment.processing | El pago pasó a processing. |
payment.requires_action | El pago necesita acción del cliente (3DS). |
payment.succeeded | El pago se cobró con éxito. |
payment.failed | El pago fue rechazado o falló. |
payment.canceled | El pago se canceló. |
refund.pending | Se inició un reembolso. |
refund.succeeded | El reembolso se completó. |
refund.failed | El reembolso falló. |
subscription.created | Se creó una suscripción. |
subscription.updated | Cambió el plan, el estado o el período de una suscripción. |
subscription.canceled | Se canceló una suscripción. |
subscription.past_due | Falló el cobro del período y la suscripción quedó past_due. |
subscription.unpaid | Se agotaron los reintentos de cobro; la suscripción quedó unpaid. |
invoice.created | Se generó una factura para un período de suscripción. |
invoice.paid | Se cobró una factura. |
invoice.payment_failed | Falló el cobro de una factura. |
provider_connection.error | Una conexión de PSP pasó a estado de error. |
Event: id, type, api_version, created_at, livemode y data.object (el recurso afectado, ej. un Payment).