Introducción
En Colombia, cada prestador de salud que quiere cobrar sus servicios a una EPS debe enviar dos documentos: la Factura Electrónica en Salud (FEV) en formato XML y el RIPS (Registro Individual de Prestaciones en Salud) en formato JSON. La normativa que define exactamente qué debe contener ese JSON —y cómo validarlo— está en la Resolución 2275 de 2023 del Ministerio de Salud, complementada por la Resolución 2284 de 2023 que establece los causales de devolución y glosa.
El problema es que esa validación, en la mayoría de las EPS del país, sigue siendo manual. Un auditor de cuentas médicas abre el archivo JSON, lo recorre campo por campo, consulta los catálogos SISPRO (CUPS, CIE-10, CUMs, habilitación IPS), verifica coherencia entre entidades, y documenta las inconsistencias. Con cientos de radicaciones mensuales, ese proceso tarda días y consume tiempo de profesionales especializados en revisar errores que podrían detectarse algorítmicamente.
Este artículo documenta la arquitectura del motor de validación que construí en GEMA, el módulo de gestión de cuentas médicas de Capital Salud EPS, durante el primer trimestre de 2026.
La decisión de diseño central: 4 niveles en cascada
La primera pregunta de diseño fue: ¿cómo organizar las reglas de validación? La tentación inicial es un validador monolítico que revise todo a la vez. Pero hay un principio operativo importante: si el JSON tiene errores de estructura (campos faltantes, tipos incorrectos), no tiene sentido ejecutar validaciones de contenido sobre datos malformados. Y si el contenido es inconsistente, ejecutar reglas de glosa sobre él genera falsos positivos.
La solución es un pipeline en cascada de 4 niveles donde cada nivel solo ejecuta si el anterior pasó:
Nivel 1 — Validación de estructura: Verifica que el JSON cumple el esquema obligatorio de la Res. 2275/2023. Campos requeridos, tipos de dato, formatos de fecha (ISO 8601), longitudes máximas. Este nivel es completamente mecánico y rápido. Un RIPS que falla aquí se devuelve de inmediato con lista de campos faltantes.
Nivel 2 — Validación de contenido: Verifica que los códigos existen en los catálogos de referencia. ¿El código CUPS está habilitado en SISPRO? ¿El diagnóstico CIE-10 es un código vigente? ¿El medicamento tiene CUM en INVIMA? ¿La IPS habilitante aparece en REPS? Este nivel requiere acceso a tablas de referencia sincronizadas. En GEMA, esas tablas se sincronizan desde SALVADOR Utilitario, un servicio independiente que mantiene los catálogos SISPRO actualizados.
Nivel 3 — Validación de relaciones: Verifica la coherencia entre entidades dentro del mismo RIPS. Cada servicio debe tener un usuario asociado. Cada medicamento debe tener una prescripción. Los diagnósticos de egreso deben ser coherentes con los de ingreso. Este es el nivel más complejo de automatizar porque requiere razonamiento sobre la estructura completa del documento, no campo a campo.
Nivel 4 — Detección de glosas FA: Aplica las 184 causales de devolución/glosa definidos en la Res. 2284/2023 para identificar inconsistencias que ameritan glosa. La diferencia con el nivel anterior es que aquí las reglas son de negocio, no de formato: fechas de atención fuera del período de cobertura, servicios no autorizados, tarifas que no corresponden al contrato.
El motor de expresiones DSL
Un diseño de motor de reglas tiene un problema de mantenimiento: si cada regla está hardcodeada en Python, agregar una nueva regla requiere un despliegue de software. En una EPS, donde los criterios de auditoría cambian con cada nueva circular del MinSalud, eso es inviable.
La solución fue un DSL (Domain Specific Language) de expresiones que permite definir reglas declarativamente. Una regla de ejemplo:
«`python
{
«id»: «RVC-045»,
«nivel»: «contenido»,
«descripcion»: «Código CUPS debe existir en catálogo SISPRO vigente»,
«expresion»: «campo(‘codigoProcedimiento’).existe_en(catalogo(‘CUPS’))»,
«severidad»: «error»,
«causal_res_2284»: «FA-08»
}
«`
El motor de expresiones (`engine/expressions.py`) parsea esa definición y la ejecuta contra el RIPS. Los auditores con conocimiento del DSL pueden agregar reglas sin escribir Python. Las 90 reglas semilla cubren los casos más frecuentes de devolución; la arquitectura está diseñada para llegar a 299 reglas en Fase II.
El problema que nadie menciona: la ambigüedad normativa
La parte técnicamente más difícil del proyecto no fue el algoritmo. Fue entender que las normas de salud en Colombia son deliberadamente ambiguas en ciertos puntos.
La Res. 2275/2023 dice, en varios artículos, que el prestador debe garantizar «la coherencia clínica entre el diagnóstico y los procedimientos registrados». Eso no es un algoritmo. Es criterio clínico. No existe una tabla de lookup que diga «si CIE-10 = J18.9 (neumonía), los CUPS permitidos son X, Y, Z».
Esta ambigüedad es intencional: la norma no puede anticipar todos los escenarios clínicos posibles. Pero crea un problema de diseño: ¿dónde termina la automatización?
La decisión que tomé fue: el motor automatiza todo lo que es mecánicamente verificable (existencia en catálogos, formatos, relaciones estructurales). Lo que requiere criterio clínico se marca como «alerta para revisión humana» en lugar de glosa automática. El objetivo no es eliminar al auditor; es que el auditor llegue a la revisión con el 80% del trabajo ya hecho.
Arquitectura de portales: PSS vs EPS
El flujo tiene dos actores con necesidades distintas: el prestador (PSS) que radica y quiere saber el estado de sus facturas, y la EPS que audita y decide. Un portal único para ambos crea fricciones de UX y complejidad de permisos innecesaria.
GEMA tiene dos portales separados construidos en React + TypeScript:
- Portal PSS: El prestador sube el FEV XML + RIPS JSON, ve el resultado de la validación automática en tiempo real, consulta el estado de sus radicaciones y recibe el informe de auditoría.
- Portal EPS: El auditor ve el dashboard de radicaciones pendientes, ejecuta validación adicional, registra la decisión (aprobación/glosa/devolución) con trazabilidad completa.
La API FastAPI detrás es la misma; los portales consumen endpoints diferenciados por rol (JWT Azure AD SSO).
Resultados de la Fase I
La Fase I entregó:
- Motor de validación 4 niveles con 30 reglas semilla RIPS + 15 reglas FEV
- Ciclo de vida de radicación completo: 9 estados, 37 causales Res. 2284
- Monitor de correo (Microsoft Graph) para pre-radicaciones automáticas desde buzón FEV
- 16 tablas de referencia en BD sincronizadas desde SALVADOR Utilitario
- Ciclo radicación → decisión: de días a menos de 24 horas
Checklist: si van a construir algo similar
- Leer completo la Res. 2275/2023 antes de escribir la primera línea de código. Las sorpresas normativas al final son costosas.
- Separar catálogos de referencia del código. Un JSON hardcodeado de CUPS que no se actualiza es peor que no tener validación.
- Diseñar el motor de reglas para ser extensible desde el inicio. Las reglas cambian; el motor no debería.
- Definir explícitamente qué es «error bloqueante» vs «alerta para revisión». No todo lo que la norma prohíbe merece una devolución automática.
- Construir portales diferenciados por rol. PSS y EPS tienen flujos de trabajo completamente distintos.
- Planificar la migración a PostgreSQL desde el MVP en SQLite. El volumen de RIPS en una EPS mediana destruye SQLite en semanas.
- Documentar cada regla con su referencia normativa. «RVC-045 — Res. 2275/2023, Art. 8, numeral 3». Cuando la norma cambie, sabrás exactamente qué reglas actualizar.
Próximos pasos (Fase II)
La Fase II está enfocada en escala: migrar a PostgreSQL, integrar los catálogos SISPRO completos vía API del MinSalud, agregar el módulo de análisis de glosas desde la UI de auditoría, y conectar con la BDUA para validación de coberturas en tiempo real.
La Fase III —motor de IA para detección de inconsistencias— está planificada pero fuera del MVP. Primero hay que tener el pipeline de datos limpio y estructurado. La IA sobre datos mal validados solo amplifica el ruido.
Si están trabajando en proyectos similares de automatización de cuentas médicas en Colombia o LATAM, me interesa mucho comparar enfoques. Pueden escribirme directamente o en los comentarios.

Deja una respuesta