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