Saltar al contenido principal

Webhooks

Propósito

Los webhooks te permiten recibir notificaciones automáticas de eventos relevantes dentro del ecosistema SPIDI sin necesidad de realizar consultas periódicas.

Permiten que SPIDI notifique en tiempo real a las aplicaciones integradas sobre eventos ocurridos en el sistema (p. ej., un pago completado, una sesión fallida o expirada).

Importante: El webhook no reemplaza la consulta del estado (GET /payment-sessions/{session_id}), sino que la complementa.

Importante: Los webhooks de SPIDI pueden tardar unos segundos en llegar a los endpoints de los comercios.

Principios de Diseño

ReglaDescripción
1. Idempotencia garantizadaTodos los webhooks incluyen Idempotency-Key y pueden reenviarse múltiples veces; el receptor debe procesarlos de forma idempotente
2. Seguridad por firmaSe calcula un HMAC-SHA256 del cuerpo con una clave secreta compartida. El receptor debe validar SPIDI-Signature y SPIDI-Timestamp
3. Reintentos controladosSi el receptor responde con código distinto de 2xx, SPIDI reintentará según política exponencial (1m → 5m → 15m → 60m máx. 4 intentos)
4. Orden garantizado por sesiónLos eventos para una misma session_id se envían en orden temporal
5. TimeoutEl servidor receptor debe responder en ≤ 5 s; de lo contrario, se considera fallo y se agenda reintento

Recomendaciones para Receptores Externos

  • Validar siempre firma y timestamp antes de procesar el evento
  • Responder 200 OK lo antes posible (ideal <2 s)
  • Procesar en background si se necesita lógica adicional
  • Registrar Idempotency-Key para evitar reprocesos
  • Consultar GET /payment-sessions/{session_id} si se requiere información completa
  • Implementar un endpoint dedicado, por ejemplo: POST https://miapp.com/webhooks/spidi

Alcance - Eventos Principales

CategoríaEventoDescripcióndoc
Sesiones de pagopayment_session.createdSe produce cuando se crea una nueva sesión de pago.payment_session.created
Sesiones de pagopayment_session.payment_completedSe produce cuando el pagador completa exitosamente el pago de una sesión.payment_session.payment_completed
Sesiones de pagopayment_session.accreditations_completedLa totalidad de los fondos ha sido acreditada al receptor o receptores.payment_session.accreditations_completed
Sesiones de pagopayment_session.accreditation_to_recipient_completedLos fondos han sido acreditados a uno de los receptores (split).payment_session.accreditation_to_recipient_completed
Sesiones de pagopayment_session.accreditation_to_recipient_failedFallo en el intento de acreditar los fondos a un receptor.payment_session.accreditation_to_recipient_failed
Sesiones de pagopayment_session.accreditation_to_recipient_completedLos fondos han sido acreditados a uno de los receptores (split).payment_session.accreditation_to_recipient_completed

Estructura General del Webhook

Headers Estándar

  • Content-Type: application/json
  • spidi-signature: sha256-hash - Firma generada con clave privada del remitente (Plataforma o SPIDI). Se usa para validar autenticidad y evitar falsificaciones.
  • spidi-timestamp: 2025-10-13T14:30:00Z - Timestamp del evento en formato ISO 8601
  • idempotency-key: e77a9dbf-0c8c-4a7a-932f-9888aa88f4e9 - UUID v4 para garantizar procesamiento idempotente

¿Para qué sirve el spidi-signature?

El Escenario 1: Ataque (Falsificación de Datos / Man-in-the-Middle)

Situación: Un atacante intercepta una comunicación o intenta hacerse pasar por SPIDI.

El Atacante: Crea un JSON falso en su computadora.

  • Evento: payment_session.paid

  • Monto: $5000.00 (aunque el pago real nunca existió).

El Envío: El atacante envía este JSON a tu endpoint /webhooks/spidi.

  • Tu Servidor (Vulnerable):

  • Recibe el JSON.

  • Lee status: paid.

  • Error: No verifica quién lo envió.

Resultado: Tu sistema libera un producto de $5000 a un estafador porque confió ciegamente en el contenido del mensaje.

El Escenario 2: Ataque (Falsificación Evitada)

Situación: Tu servidor implementa la validación de firma HMAC-SHA256.

El Atacante: Intenta lo mismo. Modifica el cuerpo del mensaje para decir que pagó $5000.00.

El Envío: El atacante necesita poner algo en el header SPIDI-Signature. Como no tiene tu Clave Secreta, inventa una firma o deja la original del mensaje anterior.

Tu Servidor (Protegido):

  • Recibe el mensaje (Raw Body).

  • Toma tu Clave Secreta (que solo tú y SPIDI tienen) y calcula matemáticamente el hash del cuerpo recibido.

  • Calculado por ti: Hash_XYZ (Basado en el cuerpo modificado).

  • Recibido en Header: Hash_ABC (Firma inventada o vieja).

  • Validación:

  • Tu código compara: ¿Hash_XYZ == Hash_ABC?

  • Respuesta: NO.

  • Acción: 401 Unauthorized.

Resultado: El mensaje es rechazado. El sistema sabe que el contenido fue alterado o no fue firmado por SPIDI.

¿Para qué sirve el spidi-timestamp?

El Escenario 1: Ataque (Replay Attack Diferido)

Imagina este escenario donde NO validas el Timestamp, solo la Idempotencia:

Día 1 (Hoy):

SPIDI te envía un webhook: "Pago de $100 recibido".

  • Idempotency-Key: A123.

  • Tu servidor procesa el pago y guarda A123 en Redis con expiración de 24 horas.

  • Resultado: Todo bien.

Día 1 (5 minutos después):

  • Un atacante interceptó ese paquete. Lo reenvía.

  • Tu servidor revisa Redis: "¿Existe A123?".

  • Respuesta: "SÍ".

  • Resultado: Tu servidor ignora la petición. La idempotencia funcionó.

Día 3 (48 horas después):

  • Tu Redis ya borró la clave A123 automáticamente porque pasó su tiempo de vida.

  • El atacante (que guardó el paquete original) lo vuelve a enviar.

  • Tu servidor revisa Redis: "¿Existe A123?".

  • Respuesta: "NO" (porque ya se borró).

  • Resultado: Tu servidor cree que es una transacción nueva. PROCESA EL PAGO OTRA VEZ.

El Escenario 2: Ataque (Replay Attack Evitado)

El Escenario del Ataque (Replay Attack Diferido) Imagina este escenario donde NO validas el Timestamp, solo la Idempotencia:

Día 1 (Hoy):

  • SPIDI te envía un webhook: "Pago de $100 recibido".

  • Idempotency-Key: A123.

  • Tu servidor procesa el pago y guarda A123 en Redis con expiración de 24 horas.

  • Resultado: Todo bien.

Día 1 (5 minutos después):

  • Un atacante interceptó ese paquete. Lo reenvía.

  • Tu servidor revisa Redis: "¿Existe A123?".

  • Respuesta: "SÍ".

  • Resultado: Tu servidor ignora la petición. La idempotencia funcionó.

Día 3 (48 horas después):

  • El atacante envía el paquete viejo.

  • El paquete dice: spidi-timestamp: 2026-02-06 (Fecha de hace 2 días).

  • Tu servidor recibe el paquete hoy (2026-02-08).

  • Tu código dice: "Espera, la Idempotencia no la encuentro (se borró), PERO este mensaje dice que fue creado hace 48 horas. Mi límite es 5 minutos."

  • Acción: 401 Unauthorized / Reject.

¿Para que sirve el idempotency-key?

El Escenario 1: Fallo (Duplicidad por falta de Idempotency-Key)

Situación: SPIDI (o el emisor) envía el webhook sin enviar la llave de idempotencia, o tu servidor ignora ese header.

El Evento:

  • SPIDI envía el webhook payment_session.paid.

  • Nota: No se envía Idempotency-Key.

Tu Servidor (Ciego):

  • Recibe la petición.

  • Crea la orden de compra #500 en tu base de datos.

  • Fallo: Justo antes de responder 200 OK, tu servidor tiene un micro-corte de internet o tarda demasiado en responder.

SPIDI:

  • Como no recibió el 200 OK a tiempo, asume que el mensaje se perdió.

  • Espera 1 minuto y hace un reintento automático.

Tu Servidor:

  • Recibe otra vez el mismo mensaje (mismo monto, mismos datos).

  • Como no hay una llave única para identificar ese mensaje específico, tu servidor piensa: "¡Genial! Otro pago nuevo".

  • Crea la orden de compra #501 (Duplicada).

Resultado: Has procesado y entregado el producto dos veces por error, perdiendo dinero o inventario.

El Escenario 2: Éxito (Idempotencia Garantizada)

Situación: SPIDI envía la llave y tu servidor la utiliza para recordar el pasado inmediato.

El Evento:

  • SPIDI envía el webhook payment_session.paid.

  • Incluye Idempotency-Key: B777-UUID-V4.

Tu Servidor (Protegido):

  • Recibe la petición.

  • Primero guarda B777-UUID-V4 en su base de datos/caché.

  • Crea la orden de compra #500.

  • Fallo: Nuevamente, la conexión se corta antes de enviar el 200 OK.

SPIDI:

  • Reintenta el envío después de 1 minuto.

  • Envía exactamente el mismo Idempotency-Key: B777-UUID-V4.

Tu Servidor:

  • Recibe el reintento.

  • Consulta la base de datos: "¿Ya he procesado la llave B777-UUID-V4?"

  • Respuesta: SÍ.

  • Acción:

  • Detiene el proceso (no crea orden nueva).

  • Simplemente responde 200 OK inmediatamente.

Resultado: Tu base de datos se mantiene limpia con una sola orden (#500) y SPIDI marca el evento como entregado exitosamente.

Body (ejemplo genérico)

Reglas y recomendaciones para el Receptor

El endpoint del receptor debe:

  • Responder con HTTP/1.1 200 OK en un máximo de 5 segundos para confirmar recepción
  • En caso contrario, SPIDI reintentará
  • Validar siempre la firma (SPIDI-Signature) y timestamp antes de procesar el evento
  • Procesar de forma idempotente usando Idempotency-Key
  • Registrar el evento para evitar reprocesos

Los webhooks que emita SPIDI o que reciba de terceros deben:

  • Incluir firma (HMAC o JWT) validable por la contraparte
  • Usar timestamp para evitar replay attacks
  • Ser idempotentes

Reintentos Automáticos

  • Cada evento se envía hasta 3 veces en caso de que el endpoint externo no responda con un código 2xx
  • Retraso incremental entre reintentos: 1 min → 5 min → 15 min → 60 min (máx. 4 intentos)
  • Si después del último intento no hay confirmación, el evento se marca como "undelivered" y queda disponible para reenvío manual desde panel administrativo (futuro)

¿Cómo puedo validar que el webhooks venga efectivamente de SPIDI?

Para validar que el webhook venga efectivamente de SPIDI puedes usar la siguiente formula:



is_valid(user_webhook_secret, event_timestamp, webhook_body_string, spidi_signature) {

// Construir la base esperada
content = event_timestamp + "." + webhook_body_string

// Calcular el HMAC-SHA256
expected = generate_hmac_sha256(user_webhook_secret, content, "hex")

// Comparar de forma segura (para evitar timing attacks)
return compare_secure(expected, spidi_signature)
}


Donde:

  • spidi_signature: es el campo 'spidi-signature' que viene en el headers del webhook

  • event_timestamp: es el 'spidi-timestamp' que viene en el headers del webhook en formato string

  • webhook_body_string: es el body del webhook en formato string

  • user_webhook_secret: El secret es el codigo secreto para validar la firma, es distinto para cada usuario, y debe ser proporcionado por el equipo de SPIDI.

Ejemplo de implementación en JavaScript (Node.js)

const crypto = require('crypto');

function isValid(secret, timestamp, payload, signature) {
const base = `${timestamp}.${payload}`;
const expected = crypto.createHmac('sha256', secret).update(base, 'utf8').digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

Ejemplo de implementación en PHP

function isValid($secret, $timestamp, $payload, $signature) {
$base = $timestamp . "." . $payload;
$expected = hash_hmac('sha256', $base, $secret);
return hash_equals($expected, $signature);
}

Ejemplo de implementación en Python

import hmac
import hashlib

def is_valid(secret: str, timestamp: str, payload: str, signature: str) -> bool:
base = f"{timestamp}.{payload}".encode('utf-8')
expected = hmac.new(secret.encode(), base, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)

Casos especificos

Creación de sesion de pago (payment_session.created)

payment_session.created

Cobro exitoso al pagador de la sesion de pago (payment_session.paid)

payment_session.paid

Acreditacion parcial (payment_session.partially_accredited)

El dinero se envió al receptor(en caso de no tener split) o uno de los receptores(en caso de split) del pago

payment_session.partially_accredited

Acreditación completa de la sesión de pago (payment_session.accredited)

El dinero se envió al receptor(en caso de no tener split) o a todos los receptores(en caso de split) del pago de forma exitosa

payment_session.accredited

Fallo en uno de los intentos de acreditación de la sesión de pago (payment_session.failed_credit)

Hubo un fallo en uno de los intentos de acreditación de la sesión de pago

payment_session.failed_credit