Saltar al contenido

Webhooks

API v0.1.0
En esta página

Rutiva envía notificaciones HTTP a tu servidor cuando cambia el estado de un payment_intent. Esencial para reconciliación, cumplimiento de pedidos y notificaciones al usuario.

Qué son

Eventos firmados con HMAC-SHA256 (estilo Stripe) sobre el body raw + timestamp. Permite verificar autoría sin compartir secretos por el canal.

Eventos emitidos

  • payment_intent.created
  • payment_intent.succeeded
  • payment_intent.failed
  • payment_intent.canceled

Registrar tu endpoint receptor

POST /v1/webhook_endpoints (requiere sk_):

json
{
  "url": "https://tu-dominio.com/webhooks/rutiva",
  "enabled_events": ["payment_intent.succeeded", "payment_intent.failed"]
}

Filtros soportados en enabled_events:

  • ["*"] — todos los eventos.
  • ["payment_intent.succeeded"] — exacto.
  • ["payment_intent.*"] — wildcard por prefijo.

Response 201 (UNA SOLA VEZ):

json
{
  "external_id": "whe_xxx",
  "url": "https://tu-dominio.com/webhooks/rutiva",
  "signing_secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxx"
}

Headers entrantes

bash
Content-Type: application/json
X-Rutiva-Signature: t=1715539200,v1=<sha256_hex>
X-Rutiva-Event-Type: payment_intent.succeeded

Verificación de firma HMAC

Algoritmo:

bash
v1 = HMAC_SHA256(signing_secret, f"{t}.{body}")

Tu handler debe:

  1. Leer el body raw (sin parsear JSON antes — la firma es sobre los bytes exactos).
  2. Recalcular HMAC con tu signing_secret guardado.
  3. Comparar con v1 usando comparación timing-safe (hmac.compare_digest / crypto.timingSafeEqual).
  4. Rechazar si t es muy viejo (> 5 min) para mitigar replays.
  5. Responder 2xx rápido (< 10s). Procesar el evento idempotentemente.

Ejemplos de handler

javascript
import crypto from "crypto";
import express from "express";

const app = express();

app.post(
  "/webhooks/rutiva",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const header = req.header("X-Rutiva-Signature");
    const [tPart, v1Part] = header.split(",");
    const t = tPart.split("=")[1];
    const v1 = v1Part.split("=")[1];
    const body = req.body.toString();
    const expected = crypto
      .createHmac("sha256", process.env.RUTIVA_WEBHOOK_SECRET)
      .update(`${t}.${body}`)
      .digest("hex");
    if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))) {
      return res.status(400).send("invalid_signature");
    }
    if (Math.abs(Date.now() / 1000 - parseInt(t, 10)) > 300) {
      return res.status(400).send("timestamp_out_of_tolerance");
    }
    const event = JSON.parse(body);
    // procesar event.type, event.data ...
    res.status(200).send("ok");
  }
);

Notas

  • TLS obligatorio en la URL receptora (https://).
  • Si tu endpoint falla (no responde 2xx), por ahora no se reintenta en MVP. Reintentos durables vienen en próxima versión.
  • El mismo evento puede llegar más de una vez en el futuro (cuando se habiliten retries). Procesa idempotentemente.