Motor de integración para instituciones de salud
Cada mensaje clínico, hasta el acuse.
Titan mueve HL7 v2, HL7 v3 y FHIR R4 entre los sistemas que ya tiene la institución, y escribe directo en su base —Oracle incluido, sin Instant Client. Sin licencia por interfaz, en su propia infraestructura, y con una respuesta verificable a la única pregunta que importa: ¿el destino lo recibió?
Lo que habla hoy
Los estándares que ya expone su HIS.
Un Millennium gestionado se alcanza de dos maneras: HL7 v2 sobre MLLP por el enlace privado, y sus APIs FHIR R4 autenticadas con OAuth2. Titan habla las dos, sin esperar un conector propietario ni pagar por abrirlo.
MSH|^~\&|HIS|CLINICA|TITAN|CLINICA|20260830090000||ORM^O01|MSG-00042|P|2.5.1 PID|0001||11111111-1^^^1|PACIENTE DE PRUEBA^TEST||19750322|F PV1|1|O|IMAGEN|||||99999-9^MEDICO DE PRUEBA^EJEMPLO ORC|NW|400-1^IMAGEN||||||20260830090000 OBR|1|400-1^IMAGEN||224^ECOTOMOGRAFIA DOPPLER^IMG
{
"mensaje": "ORM^O01",
"control_id": "MSG-00042",
"paciente": {
"documento": "11111111-1",
"nombre": "TEST PACIENTE DE PRUEBA",
"nacimiento": "1975-03-22"
},
"solicitante": "99999-9",
"examenes": [
{ "orden": "400-1", "codigo": "224", "nombre": "ECOTOMOGRAFIA DOPPLER" }
]
}
channels: - slug: orm-inbound source: type: mllp_listener port: 6679 message_type: ORM^O01 transformers: - type: field_mapper mapping: orm-o01-inbound@1.0.0 destinations: - type: http url: https://his.interno/api/ordenes outbox: true # reintentos + DLQ
Cómo se opera
Los canales se declaran, no se dibujan.
Cada integración es un archivo versionado. Se revisa como código, se aprueba como código y se despliega con un comando que primero muestra qué va a cambiar.
Plan antes de aplicar
El despliegue enumera qué canales se crean, cuáles cambian y cuáles se detienen, y falla si falta una variable. Nadie descubre una interfaz caída por un cambio que nadie recuerda haber hecho.
Git es la fuente de verdad
Quién cambió qué mapeo, cuándo y por qué, con el historial completo. Los mapeos son inmutables por versión: modificar uno obliga a publicar una versión nueva.
Corre en la tenencia de ustedes
En un servidor propio o en una VM de su propia cuenta cloud, al lado de donde termina el enlace con el HIS. No operamos ninguna nube donde aterricen sus datos: el proveedor no tiene acceso a información de pacientes en ningún momento.
Así se ve un canal por dentro. Todo esto es declaración: lo único que se escribe a mano es la transformación que el flujo realmente necesite.
# Sólo los informes de imagenología de estos exámenes siguen por el canal. - type: filter config: match: all conditions: - { field: "$.mensaje", operator: equals, value: "ORU^R01" } - { field: "$.examenes.0.codigo", operator: in, value: ["224", "581"] } - { field: "$.paciente.rut", operator: not_empty } # Lo que no cumple se registra como filtrado, no como falla del canal.
// El mismo script que sus integradores ya escriben en Mirth: recibe el // mensaje y devuelve el mensaje. Corre aislado y con tiempo máximo. message.paciente.rut = message.paciente.rut.replace(/\./g, ''); message.examenes = message.examenes.map(function (examen) { examen.codigo = examen.codigo.padStart(6, '0'); return examen; }); if (!message.examenes.length) return null; // null descarta el mensaje return message;
# Un informe con tres exámenes se entrega como tres mensajes, cada uno con # el paciente y la cabecera completos. - type: splitter config: field: "$.examenes" index_field: parte # 1, 2, 3… total_field: partes # 3 # Si un filtro posterior descarta una parte, las otras siguen. # Si una falla, el mensaje entero va a la cola de fallidos: media entrega # clínica es peor que ninguna.
# El patrón que ya usan hoy: la integración deja la orden en la base del # HIS —tabla o procedimiento— y el sistema receptor la levanta de ahí. destinations: - type: database connection_string: "oracle://titan@his-db:1521/CLINICAS_PROD" statement: "BEGIN sp_recibir_orden(:1, :2, :3); END;" params: ["$.folio", "$.paciente.rut", "$"] outbox: true # Oracle por driver propio —sin Instant Client ni OCI en el servidor—, y # también PostgreSQL, MySQL y SQL Server. Un campo que falta en el mensaje # corta la entrega: nunca se escribe un NULL en su lugar.
# Salida hacia una API FHIR gestionada, con el token renovándose solo. destinations: - type: http url: https://fhir.clinica.cl/r4/ServiceRequest headers: { Content-Type: application/fhir+json } auth: type: oauth2 token_url: https://auth.clinica.cl/oauth2/token client_id: titan-integracion private_key_file: /etc/titan/tls/titan.pem # SMART backend services scope: system/ServiceRequest.write outbox: true # reintentos + cola de fallidos
GET /api/batches/MSG-00042 { "batch_id": "MSG-00042", "status": "partial", "total": 3, "delivered": 2, "pending": 1, "failed": 0, "deliveries": [ { "id": "dlv_7f3a", "destination": "mllp", "status": "delivered", "attempts": 1 }, { "id": "dlv_7f3b", "destination": "database", "status": "delivered", "attempts": 1 }, { "id": "dlv_7f3c", "destination": "http", "status": "pending", "attempts": 2 } ] } # El contenido del mensaje no se devuelve acá: puede llevar datos del # paciente, y este endpoint es de estado, no de recuperación.
La diferencia operativa
Un acuse no es una entrega.
La mayoría de los incidentes de integración no son mensajes mal formados: son mensajes que se dieron por entregados. Titan separa las dos cosas y las registra por separado.
El mensaje entra, se valida el framing y se responde el ACK que el emisor espera. Queda persistido antes de responder.
Un reenvío del mismo mensaje dentro de la ventana configurada se descarta con acuse, sin efectos secundarios: no se duplica la orden.
La salida pasa por un outbox con reintentos y backoff. Mientras no haya confirmación del destino, el estado es "en cola", nunca "entregado".
Agotados los reintentos, el mensaje va a una cola de fallidos visible, con el motivo del destino. Se reprocesa desde ahí, no se pierde.
Cada lote recibe un identificador con el que el sistema emisor puede preguntar en qué estado quedó cada mensaje que envió.
La consola de operación
El recorrido del mensaje, etapa por etapa.
Cuando una orden no llega, la pregunta no es si el motor está arriba: es dónde se detuvo. Titan guarda cada etapa por separado —origen, cada transformador, cada destino— y las muestra con su duración y el motivo de la falla. Sin entrar por SSH a leer un log.
Recorrido
-
mllp_listener origen 2 ms
-
field_mapper transformación 6 ms
-
js_transform transformación 11 ms
-
database · his-db destino 34 ms
-
http · fhir.clinica.cl destino 1.204 ms502 desde el destino — "upstream connect error"
Entregas del lote
| database · his-db | Entregada | 1 intento | |
| mllp · pacs-imagen | Entregada | 1 intento | |
| http · fhir.clinica.cl | No entregada | 5 intentos | Reprocesar · Descartar |
El contenido del mensaje está detrás de un clic explícito y sólo para quien tiene el permiso: puede traer datos del paciente, y abrirlo queda registrado con nombre, hora y dirección.
Dos entregas llegaron y una no. El mensaje no está "fallido" en abstracto: falló un destino, por una razón concreta, y se reprocesa sin reenviar el original desde el HIS.
| Canal | Origen | Recibidos | Entregados | Fallidos | En cola | No entregados |
|---|---|---|---|---|---|---|
| mfn-prestaciones | mllp_listener:6681 | 0 | 0 | 0 | 0 | 0 |
| orm-inbound | mllp_listener:6679 | 4.812 | 4.809 | 3 | 2 | 1 |
| oru-resultados | mllp_listener:6680 | 9.104 | 9.104 | 0 | 0 | 0 |
| adt-demografia | mllp_listener:6678 | 21.446 | 21.446 | 0 | 0 | 0 |
El canal detenido va primero, después el que tiene algo sin entregar, y al final los sanos. Un panel donde hay que buscar el problema no sirve a las tres de la mañana.
Mirar no es actuar
Tres roles: quien consulta el estado y el recorrido, quien además ve el contenido y reprocesa, y quien inicia o detiene canales. El permiso se verifica en el servidor, no escondiendo el botón.
Quién abrió qué
Ver el contenido de un mensaje, reprocesar, descartar, detener un canal: cada acción queda con usuario, hora y dirección. El acceso a datos clínicos se distingue a simple vista, que es lo que un auditor viene a buscar.
Avisa al caer, no cada minuto
Canal detenido, canal sin mensajes, entregas sin entregar sobre un umbral, tasa de error sobre un porcentaje. Se notifica en las transiciones: un canal caído tres horas manda un aviso al caer y otro al volver, no ciento ochenta.
Seguridad de los datos clínicos
El transporte no se puede debilitar por descuido.
Todo lo que sale de un canal lleva datos de pacientes. Las decisiones de seguridad están tomadas en el producto, no delegadas a que cada integración se configure bien.
Cada salida HTTPS valida la cadena del certificado y el nombre del servidor, con TLS 1.2 como mínimo. No existe una opción para desactivarlo: pedirla devuelve un error que indica qué configurar en su lugar.
Para endpoints internos con autoridad certificadora institucional se declara el bundle, y para autenticación mutua el certificado de cliente. Nadie necesita apagar la verificación para que un enlace privado funcione, que es como suele abrirse este agujero.
Los secretos se leen de variables de entorno, no del archivo del canal, y los tokens viven sólo en memoria: no se escriben a disco, ni a log, ni aparecen en el detalle de un error.
MLLP es un protocolo sin cifrado propio, en Titan y en cualquier otro motor. Va dentro del enlace privado hacia el HIS — VPN o enlace dedicado — que es como se opera con un Millennium gestionado.
Cada mensaje deja registro de qué canal lo procesó, cuándo y con qué resultado, con la retención que defina la institución.
Frente a un motor licenciado por interfaz
Dónde conviene cada uno.
La comparación honesta importa más que la favorable: si la institución necesita mañana un conector empaquetado para un EHR comercial, hay productos que lo traen hecho y Titan no.
| Criterio | Motor licenciado por interfaz | Titan |
|---|---|---|
| Costo al agregar una integración | Licencia por interfaz o por conector | Sin costo por interfaz adicional |
| Dónde viven los datos clínicos | Nube del proveedor u on-premise según el contrato | Siempre en la infraestructura de la institución |
| Configuración de canales | Consola propietaria; el cambio vive dentro del producto | Archivos versionados en git, con revisión previa |
| Confirmación de entrega | Variable según el adaptador | Estado por lote, con cola de fallidos consultable |
| Acceso al código | Cerrado | Auditable por el equipo de la institución |
| Escribir en la base del HIS | Driver del proveedor: JDBC u Oracle Instant Client instalado en el servidor | Oracle, PostgreSQL, MySQL y SQL Server; Oracle con driver propio, sin cliente que instalar |
| Conectores empaquetados para EHR comerciales | Incluidos para las suites más difundidas | No; la integración se construye sobre HL7 v2 y FHIR |
| Soporte con SLA corporativo | Estructura global del proveedor | Equipo directo, acuerdo a definir |
Próximo paso
Un flujo real, seis semanas, sin compromiso de reemplazo.
La forma seria de evaluar un motor de integración no es una demo: es un flujo productivo corriendo en paralelo al actual, con tráfico real y resultados comparables mensaje a mensaje.
Elegimos un flujo acotado y de bajo riesgo. Se declara el canal y se levanta el ambiente dentro del perímetro de la institución.
Corre en paralelo al motor actual, con tráfico real espejado. Se comparan mensajes procesados, latencia y fallas.
Informe con resultados medidos, no estimados: qué se entregó, qué falló y qué costaría llevar el resto de las interfaces.