SPEI en México: arquitectura y proceso técnico
1. ¿Qué es SPEI?
El Sistema de Pagos Electrónicos Interbancarios (SPEI) es una infraestructura operada por el Banco de México que permite enviar y recibir transferencias electrónicas entre cuentas de distintas instituciones financieras participantes. Se utiliza, por ejemplo, para transferir dinero entre bancos mexicanos.
SPEI no es una aplicación bancaria ni un microservicio que cualquier empresa pueda invocar libremente. Es un sistema de pagos con reglas operativas, participantes autorizados y mecanismos de seguridad y liquidación. Una persona normalmente ordena la transferencia desde la aplicación o portal de su banco; el banco procesa la instrucción y la envía por los canales que correspondan.
En una integración real, la empresa suele conectarse con un banco, un proveedor de servicios de pago o una institución participante, según el producto contratado. No debe suponerse que una aplicación empresarial puede conectarse directamente a SPEI sin cumplir los requisitos aplicables.
2. Flujo
Un flujo simplificado puede describirse así:
- El cliente captura los datos de la transferencia: cuenta de origen, cuenta beneficiaria, importe y concepto o referencia.
- La aplicación valida el formato de los datos y presenta al cliente los detalles para confirmar.
- El backend autentica al cliente, comprueba permisos y aplica límites, reglas antifraude y validaciones de negocio.
- El sistema registra la orden de transferencia en una base de datos con un identificador único.
- Un adaptador de integración envía la instrucción al banco o proveedor conectado. En una integración real, el protocolo, las credenciales, las firmas y los mensajes dependen del contrato técnico de ese proveedor.
- La institución procesa la orden conforme a sus reglas y a la infraestructura de pagos correspondiente.
- La aplicación recibe una respuesta inicial o consulta posteriormente el estado. Una aceptación técnica de la solicitud no siempre equivale a que el dinero ya haya quedado acreditado al beneficiario.
- El backend actualiza el estado, registra los eventos y notifica al cliente.
- La conciliación compara los registros internos con los reportes o estados proporcionados por la institución.
Los estados exactos dependen del proveedor. Un modelo interno ilustrativo podría usar: CREADA, EN_PROCESO, CONFIRMADA, RECHAZADA y DESCONOCIDA. El estado DESCONOCIDA es importante: si se pierde la conexión después de enviar una orden, el sistema no debe asumir que falló, porque la institución pudo haberla recibido. Primero debe consultar o conciliar el resultado.
3. Arquitectura basada en microservicios
Sí, un sistema que ofrece transferencias SPEI puede diseñarse con microservicios, pero SPEI no exige que se use esa arquitectura. También es posible construir un sistema modular como una sola aplicación. La elección depende del volumen, la disponibilidad requerida, la organización del equipo y la complejidad operativa.
Una separación posible sería:
- API de transferencias: recibe solicitudes y devuelve el identificador de la operación.
- Servicio de identidad y autorización: verifica identidad, permisos y límites.
- Servicio de riesgo: aplica reglas antifraude y controles adicionales.
- Servicio de transferencias: administra el ciclo de vida y las transiciones de estado.
- Adaptador bancario: encapsula la API, el protocolo y las particularidades del banco o proveedor.
- Servicio de notificaciones: informa al cliente sin bloquear el procesamiento principal.
- Servicio de conciliación: compara operaciones internas con los reportes externos.
- Base de datos o libro de movimientos: conserva órdenes, estados, referencias y registros auditables.
No es obligatorio que cada elemento sea un microservicio independiente. Separarlos demasiado pronto aumenta la latencia, los fallos distribuidos y la complejidad de operación. Una buena regla es separar componentes cuando existe una razón clara de escalabilidad, aislamiento, propiedad del dominio o despliegue independiente.
Flujo técnico sugerido:
Cliente → API de transferencias → validaciones → registro persistente → cola de eventos → adaptador bancario → banco/proveedor.
Las respuestas y eventos externos actualizan el estado de la operación. Las notificaciones se envían después del registro del cambio, y la conciliación sirve como control adicional ante mensajes perdidos o estados inciertos.
4. Pseudocódigo
funcion crearTransferencia(cliente, beneficiario, importe, claveIdempotencia):
autenticar(cliente)
verificarPermiso(cliente, beneficiario)
validarImporte(importe)
validarBeneficiario(beneficiario)
aplicarLimitesYRiesgo(cliente, importe)
iniciarTransaccionBaseDeDatos:
si existeOperacionCon(claveIdempotencia):
devolver operacionExistente
operacion = guardarOperacion(
estado = "CREADA",
cliente = cliente,
beneficiario = beneficiario,
importe = importe,
clave = claveIdempotencia
)
guardarEventoOutbox("TRANSFERENCIA_CREADA", operacion.id)
confirmarTransaccionBaseDeDatos
devolver operacion.id
procesadorTransferencias():
para cada evento pendiente:
operacion = cargarOperacion(evento.operacionId)
si operacion ya fue enviada:
continuar
marcar operacion como "EN_PROCESO"
resultado = adaptadorBancario.enviar(operacion)
si resultado es confirmacion definitiva:
actualizar estado a "CONFIRMADA"
si resultado es rechazo definitivo:
actualizar estado a "RECHAZADA"
si resultado es ambiguo o hubo timeout:
actualizar estado a "DESCONOCIDA"
programar consulta de estado o conciliacion
registrar auditoria
publicar evento de estado actualizado
La clave de idempotencia evita que un reintento del cliente cree otra transferencia idéntica. El patrón Outbox ayuda a que el registro de la operación y la publicación del evento no queden descoordinados si ocurre un fallo entre ambas acciones. La lógica de envío debe contemplar reintentos seguros y consultas de estado; no se debe reenviar ciegamente una orden cuyo resultado sea incierto.
5. Ejemplo en Java
El siguiente ejemplo modela una capa de aplicación y un adaptador bancario abstracto. No se conecta a SPEI, no incluye credenciales, no implementa un protocolo bancario real y no mueve dinero. En producción, el adaptador debe implementarse según la documentación y los requisitos de la institución contratada.
import java.math.BigDecimal;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
enum Estado {
CREADA, EN_PROCESO, CONFIRMADA, RECHAZADA, DESCONOCIDA
}
record SolicitudTransferencia(
String clienteId,
String cuentaDestino,
BigDecimal importe,
String claveIdempotencia
) {}
record Transferencia(
String id,
String clienteId,
String cuentaDestino,
BigDecimal importe,
Estado estado
) {}
enum ResultadoBanco {
ACEPTADA, RECHAZADA, INCIERTO
}
interface AdaptadorBancario {
ResultadoBanco enviar(Transferencia transferencia);
}
class TransferenciasService {
private final Map<String, Transferencia> porClave = new ConcurrentHashMap<>();
private final AdaptadorBancario banco;
TransferenciasService(AdaptadorBancario banco) {
this.banco = banco;
}
public Transferencia crear(SolicitudTransferencia solicitud) {
validar(solicitud);
// computeIfAbsent ilustra la idempotencia en este proceso local.
// En producción, la unicidad debe garantizarse también en la base de datos.
return porClave.computeIfAbsent(
solicitud.claveIdempotencia(),
clave -> new Transferencia(
UUID.randomUUID().toString(),
solicitud.clienteId(),
solicitud.cuentaDestino(),
solicitud.importe(),
Estado.CREADA
)
);
}
public Estado procesar(String claveIdempotencia) {
Transferencia actual = porClave.get(claveIdempotencia);
if (actual == null) {
throw new IllegalArgumentException("Transferencia no encontrada");
}
Transferencia enProceso = conEstado(actual, Estado.EN_PROCESO);
porClave.put(claveIdempotencia, enProceso);
ResultadoBanco resultado = banco.enviar(enProceso);
Estado nuevoEstado = switch (resultado) {
case ACEPTADA -> Estado.CONFIRMADA;
case RECHAZADA -> Estado.RECHAZADA;
case INCIERTO -> Estado.DESCONOCIDA;
};
porClave.put(claveIdempotencia, conEstado(enProceso, nuevoEstado));
return nuevoEstado;
}
private static Transferencia conEstado(Transferencia t, Estado estado) {
return new Transferencia(
t.id(), t.clienteId(), t.cuentaDestino(), t.importe(), estado
);
}
private static void validar(SolicitudTransferencia s) {
if (s == null) throw new IllegalArgumentException("Solicitud obligatoria");
if (s.clienteId() == null || s.clienteId().isBlank())
throw new IllegalArgumentException("Cliente obligatorio");
if (s.cuentaDestino() == null || s.cuentaDestino().isBlank())
throw new IllegalArgumentException("Cuenta destino obligatoria");
if (s.importe() == null || s.importe().signum() <= 0)
throw new IllegalArgumentException("El importe debe ser positivo");
if (s.claveIdempotencia() == null || s.claveIdempotencia().isBlank())
throw new IllegalArgumentException("Clave de idempotencia obligatoria");
}
}
Este código sirve para entender la estructura, pero no debe usarse para procesar dinero real. El almacenamiento es solo memoria local, no hay autenticación, límites, cifrado, auditoría durable, transacciones distribuidas ni consulta real de estados. Además, el ejemplo simplificado puede volver a invocar al adaptador si se llama repetidamente a procesar; una implementación productiva debe persistir el estado de envío y controlar los reintentos para evitar duplicados.
6. Controles en producción
- Idempotencia: la misma solicitud repetida debe devolver la operación existente, no crear otra.
- Persistencia transaccional: guardar la orden y sus eventos de forma consistente.
- Estados explícitos: diferenciar aceptación inicial, confirmación definitiva, rechazo y resultado desconocido.
- Reintentos controlados: aplicar límites, espera incremental y reglas específicas del proveedor.
- Conciliación: resolver operaciones cuyo resultado no llegó de forma concluyente.
- Seguridad: autenticar y autorizar, proteger secretos, cifrar comunicaciones y limitar el acceso a datos financieros.
- Auditoría: conservar quién ordenó la operación, cuándo, por qué canal y qué cambios de estado ocurrieron.
- Observabilidad: usar métricas, logs estructurados, trazas y alertas sin exponer datos sensibles.
- Pruebas: cubrir timeouts, mensajes duplicados, respuestas tardías, fallos de base de datos y caídas del proveedor.
- Cumplimiento: revisar las obligaciones legales, operativas y contractuales aplicables con la institución participante y asesores especializados.
Una respuesta HTTP exitosa de la API propia solo significa que la aplicación aceptó o registró la solicitud, según el contrato de esa API. No debe presentarse como comprobante de que la transferencia SPEI se liquidó o se acreditó.
7. Conclusión
SPEI es la infraestructura interbancaria mexicana que permite transferencias electrónicas entre participantes. Una aplicación que ofrece esta funcionalidad normalmente coordina validaciones, registro, integración con una institución, seguimiento del estado y conciliación. Los microservicios son una opción arquitectónica, no un requisito de SPEI.
La dificultad central no consiste únicamente en enviar una petición: consiste en mantener un estado financiero correcto ante reintentos, duplicados, timeouts, respuestas tardías y diferencias entre los registros internos y los externos. Por eso, idempotencia, persistencia, auditoría, conciliación y manejo explícito de resultados inciertos son elementos fundamentales.
Fuentes de consulta
- Banco de México, información oficial sobre SPEI: https://www.banxico.org.mx/servicios/sistema-pagos-electronicos-in.html
- Banco de México, portal institucional: https://www.banxico.org.mx/
- Documentación técnica del banco o proveedor de pagos con el que se contrate la integración: debe consultarse para conocer APIs, formatos, autenticación, estados, tiempos y procedimientos reales.
Nota: el flujo y el código de este artículo son didácticos. Los nombres de estados y la arquitectura propuesta son ilustrativos; no representan una especificación oficial de SPEI ni sustituyen la documentación técnica y normativa vigente.
© Arturo Juárez Monroy