PayForcePayForce

Webhooks

PayForce avisa a tu servidor con un POST JSON. Registra la URL en el dashboard o con POST /api/v1/webhooks. Guarda el secret: solo se muestra al crearlo.

Eventos

  • payment.succeeded — el cobro entró.
  • payment.failed — el cobro fue rechazado.
  • payment.refunded — hay una devolución.
  • checkout.completed — la sesión de checkout terminó.
  • dispute.created — el comprador abrió una disputa.

Cabeceras

  • X-PayForce-Event — nombre del evento.
  • X-PayForce-Delivery-Id — id de este intento de entrega.
  • X-PayForce-Signature — t=<ms>,v1=<hex>.

Formato del payload

{
  "event": "payment.succeeded",
  "payment": {
    "id": "pf_pay_test",
    "amount": 1990,
    "currency": "eur",
    "status": "succeeded",
    "description": "Evento de prueba",
    "customer_email": "test@example.com",
    "created_at": "2026-09-11T14:06:02.000Z"
  }
}

payment.failed añade failure_code. checkout.completed envía object con payment_id. dispute.created envía dispute.

Verificar la firma HMAC

La firma es HMAC-SHA256 de timestamp + "." + body, en hex. Tolera 300 segundos de desfase de reloj. Compara en tiempo constante.

const crypto = require("crypto");

function verifyPayForceSignature(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(",").map((p) => {
      const i = p.indexOf("=");
      return [p.slice(0, i).trim(), p.slice(i + 1)];
    }),
  );
  const t = parts.t;
  const v1 = parts.v1;
  if (!t || !v1) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");

  const a = crypto.createHash("sha256").update(expected).digest();
  const b = crypto.createHash("sha256").update(v1).digest();
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return false;

  const ts = Number(t);
  const tsSec = ts > 1e12 ? Math.floor(ts / 1000) : Math.floor(ts);
  return Math.abs(Math.floor(Date.now() / 1000) - tsSec) <= 300;
}

app.post("/webhooks/payforce", express.raw({ type: "application/json" }), (req, res) => {
  const rawBody = req.body.toString("utf8");
  const ok = verifyPayForceSignature(
    rawBody,
    req.get("x-payforce-signature") || "",
    process.env.PAYFORCE_WEBHOOK_SECRET,
  );
  if (!ok) return res.status(400).send("invalid signature");
  const event = JSON.parse(rawBody);
  // … marca el pedido …
  res.json({ received: true });
});

Con el SDK: pf.webhooks.verify(rawBody, header, secret).

Reintentos

Si tu endpoint no responde 2xx, PayForce reintenta 6 veces. La espera entre intentos es 1 min, 5 min, 30 min, 2 h, 12 h y 24 h. El timeout de cada POST es de 10 segundos. Un 3xx no cuenta como éxito.

Usa X-PayForce-Delivery-Id para no procesar dos veces el mismo envío. El mismo evento puede llegar más de una vez si el primer 2xx se perdió al registrar la entrega.