API SATI-Q · Recolección de datos del programa
V2026 - release · Publicado por Hardineros SAS
API SATI-Q · Contrato V2026. Esta página se genera a partir del cuestionario publicado: siempre refleja lo que el servidor exige hoy.
| URL canónica del cuestionario: https://fhir.hardineros.net/fhir/Questionnaire/eds-2026 | Versión: V2026 | |
| Active desde 2026-08-12 | Versión FHIR: 4.0.1 | |
Cómo conectar el sistema informático de un centro para que envíe sus episodios de UCI de forma automática, usando FHIR R4.
Esta guía es para el equipo de desarrollo del sistema de un centro que participa del programa. Se asume experiencia con APIs REST y JSON; no hace falta conocer FHIR.
Un episodio de internación en UCI ya finalizado, con el conjunto mínimo de datos del EDS: identificación interna del paciente, fechas de ingreso y egreso, datos demográficos, motivo y procedencia, score de gravedad, dispositivos con sus días, complicaciones y resultado al egreso.
No se envían datos identificatorios del paciente. El identificador que viaja es el número interno con el que el centro lo reconoce en su propio sistema.
| Base de la API | https://fhir.hardineros.net/fhir |
|---|---|
| Endpoint de token | https://fhir.hardineros.net/oauth/token |
| Versión FHIR | R4 (4.0.1) |
| Formato | application/fhir+json |
Las genera el propio centro desde el formulario web de SATI-Q, en la opción Conexión con mi sistema del menú. No hay que pedirlas a nadie: quien tiene el usuario del centro las genera y las entrega al equipo de desarrollo.
Se obtienen dos valores: un client_id, que identifica al cliente, y un client_secret, que es la clave.
Cada credencial queda atada a un centro. El servidor determina a qué centro corresponde cada episodio a partir de las credenciales, no de un campo del envío: un cliente no puede cargar ni leer episodios de otro centro.
$validate es de acceso libre y no guarda nada. Toda la construcción del recurso se puede desarrollar y probar contra ese endpoint antes de tener claves.La API usa OAuth2 con el flujo client credentials, pensado para comunicación entre sistemas sin intervención de una persona.
Las credenciales pueden ir en el encabezado Authorization como Basic, que es lo recomendado, o en el cuerpo del pedido.
POST https://fhir.hardineros.net/oauth/token Authorization: Basic BASE64(client_id:client_secret) Content-Type: application/x-www-form-urlencoded grant_type=client_credentials
La respuesta:
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "episodio.read episodio.write"
}Va en el encabezado de cada pedido a QuestionnaireResponse.
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...
| episodio.read | Leer episodios ya cargados por el centro |
|---|---|
| episodio.write | Enviar episodios nuevos y corregir los ya enviados |
| episodio.delete | Eliminar episodios. No se concede por defecto: hay que pedirlo al generar la credencial |
401 con un token que parecía válido, pedir uno nuevo y reintentar una vez.client_secret no debe quedar en el código fuente ni en un repositorio: va en configuración del entorno.Todo lo que el servidor exige está declarado en el recurso Questionnaire. No hay validaciones ocultas.
GET https://fhir.hardineros.net/fhir/Questionnaire/eds-2026
Ahí figuran los campos con su tipo, cuáles son obligatorios, los rangos, las opciones de cada campo codificado, las dependencias entre campos y las reglas que combinan varios. Conviene leerlo desde el sistema en lugar de copiar esta página a mano, sobre todo las listas de códigos: si el EDS cambia, el cuestionario lo refleja.
El recurso a enviar es un QuestionnaireResponse:
{
"resourceType": "QuestionnaireResponse",
"questionnaire": "https://fhir.hardineros.net/fhir/Questionnaire/eds-2026",
"status": "completed",
"item": [
{
"linkId": "internacion",
"item": [
{ "linkId": "FECHING", "answer": [{ "valueDate": "2026-06-03" }] }
]
}
]
}questionnaire debe apuntar a la URL canónica exacta. Se admite con sufijo de versión, por ejemplo terminando en |V2026.status debe ser completed o amended.linkId y un arreglo answer con un único elemento.| Tipo | Elemento | Formato |
|---|---|---|
| Fecha | valueDate | AAAA-MM-DD |
| Hora | valueTime | HH:MM |
| Entero | valueInteger | Número sin comillas |
| Decimal | valueDecimal | Número sin comillas, punto decimal |
| Sí o no | valueBoolean | true / false |
| Codificado | valueCoding | Objeto con system y code |
Un campo codificado se ve así:
{
"linkId": "TIPO",
"answer": [{
"valueCoding": {
"system": "https://fhir.hardineros.net/fhir/CodeSystem/satiq-tipo",
"code": "A"
}
}]
}No existe un campo para indicarlo: lo determina el servidor a partir de las credenciales.
ESTADIA es obligatoria y se guarda tal como se envía. El servidor no la recalcula a partir de las fechas ni la corrige. Tampoco tiene tope: sólo se exige que sea 1 o más, igual que los días de cada dispositivo.
Las quince banderas de dispositivos y complicaciones son obligatorias y hay que enviarlas todas, incluso las que valen false. Omitir una es un error, no una forma de decir que no ocurrió.
Los 48 campos del cuestionario. Los que tienen condición sólo se exigen si esa condición se cumple, y enviarlos cuando no corresponde también es un error.
| linkId | Tipo | Oblig. | Restricciones |
|---|---|---|---|
| IDPACIENTE | integer | Sí | de 1 a 999999999 |
| REINGRESO | boolean | Sí | — |
| linkId | Tipo | Oblig. | Restricciones |
|---|---|---|---|
| FECHING | date | Sí | — |
| HORAING | time | Sí | — |
| FECEGR | date | Sí | — |
| HORAEGR | time | Sí | — |
| ESTADIA | integer | Sí | 1 o más |
| linkId | Tipo | Oblig. | Restricciones |
|---|---|---|---|
| TIPO | choice | Sí | satiq-tipo: A, P, N |
| EDAD | integer | Sí | — |
| SEXO | choice | Sí | satiq-sexo: M, F, O |
| linkId | Tipo | Oblig. | Restricciones |
|---|---|---|---|
| MOTING | choice | Sí | satiq-moting: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 99 |
| PROCEDENCIA | choice | Sí | satiq-procedencia: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 |
| linkId | Tipo | Oblig. | Restricciones |
|---|---|---|---|
| SCORE | decimal | Sí | — |
| PROBABMORT | decimal | Sí | de 0 a 99.99 |
| linkId | Tipo | Oblig. | Restricciones |
|---|---|---|---|
| VI | boolean | Sí | — |
| DIASVI | integer | Sí | 1 o más · solo si VI es true |
| VNI | boolean | Sí | — |
| DIASVNI | integer | Sí | 1 o más · solo si VNI es true |
| CAFO | boolean | Sí | — |
| DIASCAFO | integer | Sí | 1 o más · solo si CAFO es true |
| CVC | boolean | Sí | — |
| DIASCVC | integer | Sí | 1 o más · solo si CVC es true |
| SV | boolean | Sí | — |
| DIASSV | integer | Sí | 1 o más · solo si SV es true |
| SNG | boolean | Sí | — |
| DIASSNG | integer | Sí | 1 o más · solo si SNG es true |
| SE | boolean | Sí | — |
| DIASSE | integer | Sí | 1 o más · solo si SE es true |
| linkId | Tipo | Oblig. | Restricciones |
|---|---|---|---|
| NEUMONIA | boolean | Sí | — |
| NEUMONIANUM | integer | Sí | de 1 a 99 · solo si NEUMONIA es true |
| AUTOEXTUBACION | boolean | Sí | — |
| AUTOEXTUBACIONNUM | integer | Sí | de 1 a 99 · solo si AUTOEXTUBACION es true |
| INFCATETER | boolean | Sí | — |
| INFCATETERNUM | integer | Sí | de 1 a 99 · solo si INFCATETER es true |
| INFURINARIA | boolean | Sí | — |
| INFURINARIANUM | integer | Sí | de 1 a 99 · solo si INFURINARIA es true |
| ESCARAS | boolean | Sí | — |
| ESCARASNUM | integer | Sí | de 1 a 99 · solo si ESCARAS es true |
| INFHERIDAS | boolean | Sí | — |
| INFHERIDASNUM | integer | Sí | de 1 a 99 · solo si INFHERIDAS es true |
| DESLIZSNG | boolean | Sí | — |
| DESLIZSNGNUM | integer | Sí | de 1 a 99 · solo si DESLIZSNG es true |
| DESLIZCAMA | boolean | Sí | — |
| DESLIZCAMANUM | integer | Sí | de 1 a 99 · solo si DESLIZCAMA es true |
| linkId | Tipo | Oblig. | Restricciones |
|---|---|---|---|
| TISSMIN | decimal | No | de 0 a 77 |
| TISSMAX | decimal | No | de 0 a 77 |
| TISSPROMEDIO | decimal | No | de 0 a 77 |
| linkId | Tipo | Oblig. | Restricciones |
|---|---|---|---|
| RESULTADO | choice | Sí | satiq-resultado: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 |
Cada campo con opciones tiene su sistema publicado. Ese valor es el que va en system dentro del Coding.
GET https://fhir.hardineros.net/fhir/CodeSystem/satiq-tipo
| Código | Significado |
|---|---|
| A | Adulto (APACHE II) |
| P | Pediátrico (PIM3) |
| N | Neonatal (PIM3) |
GET https://fhir.hardineros.net/fhir/CodeSystem/satiq-sexo
| Código | Significado |
|---|---|
| M | Masculino |
| F | Femenino |
| O | Otro |
GET https://fhir.hardineros.net/fhir/CodeSystem/satiq-moting
Los códigos 1 a 4 son para pacientes adultos y los 5 a 10 para pediátricos y neonatales. El 99 vale para cualquiera. Usar un código que no corresponde al tipo de paciente es un error.
| Código | Significado |
|---|---|
| 1 | Patología Médica |
| 2 | Cirugía de Urgencia |
| 3 | Cirugía Programada |
| 4 | Politrauma |
| 5 | Respiratorio |
| 6 | Cardiológico |
| 7 | Neurológico |
| 8 | Postquirúrgico |
| 9 | Causa Externa |
| 10 | Otros |
| 99 | Desconocido |
GET https://fhir.hardineros.net/fhir/CodeSystem/satiq-procedencia
| Código | Significado |
|---|---|
| 1 | Guardia |
| 2 | Sector de Clínica |
| 3 | Sala de Parto |
| 4 | Piso de otro Hospital |
| 5 | UTI de otro Hospital |
| 6 | Terapia Intermedia |
| 7 | Sector de Cirugía |
| 8 | Vía Pública |
| 9 | Unidad Coronaria |
| 10 | Quirófano Programado |
| 11 | Quirófano de Urgencia |
| 12 | Otra |
| 13 | Atención Domiciliaria |
| 14 | Guardia de otro Hospital |
GET https://fhir.hardineros.net/fhir/CodeSystem/satiq-resultado
| Código | Significado |
|---|---|
| 1 | Alta al piso de internación |
| 2 | Alta Domiciliaria |
| 3 | Alta Voluntaria |
| 4 | Se traslada a otra Institución |
| 5 | Fallece |
| 6 | Terapia Intermedia |
| 7 | Unidad Coronaria |
| 8 | Otra |
| 9 | Homecare |
| 10 | Se traslada a otra UCI |
| 11 | Centro de atención del paciente crónico |
Además de la estructura, el servidor verifica reglas que combinan varios campos. Están declaradas en el propio cuestionario como extensiones questionnaire-constraint, y cada una tiene una clave que aparece en el error cuando se incumple.
| Clave | Regla |
|---|---|
| egreso-posterior-a-ingreso | La fecha y hora de egreso no pueden ser anteriores a las de ingreso. |
| edad-por-tipo | La edad debe corresponder al tipo de paciente: adulto 16 a 150 años, pediátrico 1 a 216 meses, neonatal 0 a 28 días. |
| score-por-tipo | El score debe corresponder al tipo de paciente: APACHE II de 0 a 71 en adultos, PIM3 de -6 a 25 en pediátricos y neonatales. |
| moting-por-tipo | El motivo de ingreso debe corresponder al tipo de paciente: códigos 1 a 4 en adultos, 5 a 10 en pediátricos y neonatales, 99 desconocido en cualquiera. |
| ventilatorios-no-superan-estadia | Los días de cada apoyo ventilatorio (VI, VNI, CAFO) no pueden superar la estadía informada. Los tres pueden superponerse entre sí. |
| dispositivo-no-supera-estadia | Los días de cada dispositivo no ventilatorio (CVC, SV, SNG, SE) no pueden superar la estadía informada. |
| tiss-ordenado | El TISS-28 mínimo no puede superar al promedio, ni el promedio al máximo. |
| tiss-en-rango | El TISS-28 mínimo, máximo y promedio deben estar entre 0 y 77. |
| estadia-hasta-365 | La estadía no puede superar los 365 días. |
La edad se expresa en la unidad que corresponde al tipo de paciente: años para adultos, meses para pediátricos y días para neonatales. El campo es siempre un entero; la unidad la deduce el servidor del tipo.
Estos controles no están en el cuestionario, porque necesitan mirar lo que el centro ya tiene cargado:
422.409.409 indicando con cuál choca. Egresar y reingresar el mismo día es válido: los extremos que se tocan no cuentan como superposición.$validate, porque esa operación no pide credenciales y por lo tanto no sabe de qué centro se trata. Un recurso puede dar válido y aun así ser rechazado al enviarlo.$validate corre las validaciones del cuestionario y sus reglas, pero no guarda nada y no requiere credenciales.
POST https://fhir.hardineros.net/fhir/QuestionnaireResponse/$validate
Content-Type: application/fhir+json
{ ...el QuestionnaireResponse... }Siempre responde 200. Lo que cambia es el contenido: si el recurso está bien, un único issue con severity information; si no, un issue por cada problema.
POST https://fhir.hardineros.net/fhir/QuestionnaireResponse
Authorization: Bearer TOKEN
Content-Type: application/fhir+json
{ ...el QuestionnaireResponse... }Si sale bien, la respuesta es 201, el encabezado Location trae la URL del recurso creado y el cuerpo devuelve el episodio tal como quedó guardado. El último segmento de esa URL es el identificador que conviene conservar.
GET https://fhir.hardineros.net/fhir/QuestionnaireResponse/{id}
Authorization: Bearer TOKENDevuelve el episodio reconstruido desde lo que está guardado, no una copia literal de lo enviado: si fue corregido después desde el formulario web, la lectura refleja el estado actual.
Requiere el alcance episodio.write.
PUT https://fhir.hardineros.net/fhir/QuestionnaireResponse/{id}
Authorization: Bearer TOKEN
Content-Type: application/fhir+json
{ ...el QuestionnaireResponse completo... }Reemplaza el recurso completo: hay que enviar todos los campos, no sólo los que cambian. Si el recurso trae id, tiene que ser el mismo de la dirección. Responde 200 con el episodio tal como quedó guardado.
Se aplican las mismas validaciones que en el alta, más los controles de episodio repetido y de superposición, que ignoran al propio episodio que se está corrigiendo.
Requiere el alcance episodio.delete, que no se concede por defecto.
DELETE https://fhir.hardineros.net/fhir/QuestionnaireResponse/{id}
Authorization: Bearer TOKENResponde 204 sin cuerpo. Borra el episodio junto con su TISS día por día y su historial de envíos, y no tiene vuelta atrás. Eliminar algo que ya no existe también devuelve 204: el resultado buscado ya se cumplió.
422. El centro puede reabrir el año desde el formulario web.Cada POST que llega es un alta. Ahora bien, no se puede cargar dos veces el mismo paciente con la misma fecha y hora de ingreso: el segundo intento devuelve 409. Ante un error de red del que no se sabe si el envío llegó, conviene reintentar: si el primero había entrado, el 409 lo confirma.
Todos los errores de la API se devuelven como OperationOutcome. Los del endpoint de token siguen el formato de OAuth2, con error y error_description.
| Código | Significado | Qué hacer |
|---|---|---|
| 400 | JSON inválido o recurso mal formado | Corregir el envío. No reintentar igual |
| 401 | Token ausente, vencido, inválido o cliente dado de baja | Pedir un token nuevo y reintentar una vez |
| 403 | El token no tiene el permiso necesario | Revisar los permisos del cliente |
| 404 | El episodio no existe o es de otro centro | Verificar el identificador |
| 409 | Episodio repetido, o superpuesto con otro del mismo paciente | Revisar si ya está cargado o corregir las fechas |
| 422 | No cumple el cuestionario o una regla | Corregir según los issues devueltos |
| 500 | Error del servidor | Reintentar más tarde y avisar al programa |
{
"resourceType": "OperationOutcome",
"issue": [{
"severity": "error",
"code": "business-rule",
"details": {
"coding": [{
"system": ".../CodeSystem/satiq-validacion",
"code": "edad-por-tipo"
}],
"text": "La edad debe corresponder al tipo de paciente..."
},
"diagnostics": "Edad fuera de rango para paciente neonatal: debe estar entre 0 y 28 dias.",
"expression": ["QuestionnaireResponse.repeat(item).where(linkId='EDAD')"]
}]
}code indica la naturaleza: required si falta un campo obligatorio, invalid si la estructura está mal, business-rule si se incumple una regla.details.coding.code es la clave de la regla, la misma que figura en el cuestionario. Sirve para mapear el error a un mensaje propio.expression apunta al campo que hay que corregir.diagnostics es el texto explicativo, pensado para mostrarle a una persona.Adulto de 74 años, ingreso por cirugía de urgencia, doce días de internación, con ventilación invasiva y no invasiva, tres complicaciones y fallecimiento al egreso. Es un envío válido: se puede probar tal cual contra $validate.
{
"resourceType": "QuestionnaireResponse",
"questionnaire": "https://fhir.hardineros.net/fhir/Questionnaire/eds-2026",
"status": "completed",
"authored": "2026-06-16T09:12:00-03:00",
"item": [
{
"linkId": "identificacion",
"item": [
{ "linkId": "IDPACIENTE", "answer": [{ "valueInteger": 48217 }] },
{ "linkId": "REINGRESO", "answer": [{ "valueBoolean": false }] }
]
},
{
"linkId": "internacion",
"item": [
{ "linkId": "FECHING", "answer": [{ "valueDate": "2026-06-03" }] },
{ "linkId": "HORAING", "answer": [{ "valueTime": "23:40" }] },
{ "linkId": "FECEGR", "answer": [{ "valueDate": "2026-06-15" }] },
{ "linkId": "HORAEGR", "answer": [{ "valueTime": "06:20" }] },
{ "linkId": "ESTADIA", "answer": [{ "valueInteger": 12 }] }
]
},
{
"linkId": "paciente",
"item": [
{ "linkId": "TIPO", "answer": [{ "valueCoding": { "system": "https://fhir.hardineros.net/fhir/CodeSystem/satiq-tipo", "code": "A" } }] },
{ "linkId": "EDAD", "answer": [{ "valueInteger": 74 }] },
{ "linkId": "SEXO", "answer": [{ "valueCoding": { "system": "https://fhir.hardineros.net/fhir/CodeSystem/satiq-sexo", "code": "M" } }] }
]
},
{
"linkId": "ingreso",
"item": [
{ "linkId": "MOTING", "answer": [{ "valueCoding": { "system": "https://fhir.hardineros.net/fhir/CodeSystem/satiq-moting", "code": "2" } }] },
{ "linkId": "PROCEDENCIA", "answer": [{ "valueCoding": { "system": "https://fhir.hardineros.net/fhir/CodeSystem/satiq-procedencia", "code": "11" } }] }
]
},
{
"linkId": "gravedad",
"item": [
{ "linkId": "SCORE", "answer": [{ "valueDecimal": 24 }] },
{ "linkId": "PROBABMORT", "answer": [{ "valueDecimal": 46.8 }] }
]
},
{
"linkId": "dispositivos",
"item": [
{ "linkId": "VI", "answer": [{ "valueBoolean": true }] },
{ "linkId": "DIASVI", "answer": [{ "valueInteger": 9 }] },
{ "linkId": "VNI", "answer": [{ "valueBoolean": true }] },
{ "linkId": "DIASVNI", "answer": [{ "valueInteger": 2 }] },
{ "linkId": "CAFO", "answer": [{ "valueBoolean": false }] },
{ "linkId": "CVC", "answer": [{ "valueBoolean": true }] },
{ "linkId": "DIASCVC", "answer": [{ "valueInteger": 11 }] },
{ "linkId": "SV", "answer": [{ "valueBoolean": true }] },
{ "linkId": "DIASSV", "answer": [{ "valueInteger": 12 }] },
{ "linkId": "SNG", "answer": [{ "valueBoolean": true }] },
{ "linkId": "DIASSNG", "answer": [{ "valueInteger": 10 }] },
{ "linkId": "SE", "answer": [{ "valueBoolean": true }] },
{ "linkId": "DIASSE", "answer": [{ "valueInteger": 8 }] }
]
},
{
"linkId": "complicaciones",
"item": [
{ "linkId": "NEUMONIA", "answer": [{ "valueBoolean": true }] },
{ "linkId": "NEUMONIANUM", "answer": [{ "valueInteger": 1 }] },
{ "linkId": "AUTOEXTUBACION", "answer": [{ "valueBoolean": false }] },
{ "linkId": "INFCATETER", "answer": [{ "valueBoolean": false }] },
{ "linkId": "INFURINARIA", "answer": [{ "valueBoolean": true }] },
{ "linkId": "INFURINARIANUM", "answer": [{ "valueInteger": 1 }] },
{ "linkId": "ESCARAS", "answer": [{ "valueBoolean": true }] },
{ "linkId": "ESCARASNUM", "answer": [{ "valueInteger": 2 }] },
{ "linkId": "INFHERIDAS", "answer": [{ "valueBoolean": false }] },
{ "linkId": "DESLIZSNG", "answer": [{ "valueBoolean": false }] },
{ "linkId": "DESLIZCAMA", "answer": [{ "valueBoolean": false }] }
]
},
{
"linkId": "tiss",
"item": [
{ "linkId": "TISSMIN", "answer": [{ "valueDecimal": 22 }] },
{ "linkId": "TISSMAX", "answer": [{ "valueDecimal": 41 }] },
{ "linkId": "TISSPROMEDIO", "answer": [{ "valueDecimal": 31.5 }] }
]
},
{
"linkId": "egreso",
"item": [
{ "linkId": "RESULTADO", "answer": [{ "valueCoding": { "system": "https://fhir.hardineros.net/fhir/CodeSystem/satiq-resultado", "code": "5" } }] }
]
}
]
}Sí. Un paciente puede reingresar las veces que haga falta, incluso varias en el mismo año. Lo único que no se acepta es repetir el mismo momento de ingreso, o informar un período que se superponga con otra internación suya en esa unidad.
No. Ni la estadía ni los días de cada dispositivo tienen máximo: sólo se exige que sean 1 o más. Las cantidades de complicaciones sí están limitadas a 99.
Sí, mientras ese año no esté cerrado para el centro. Al cerrar un año deja de aceptarse cualquier episodio con egreso en él.
Se corrige con un PUT sobre su dirección, enviando el recurso completo, o desde el formulario web. Si el año está cerrado, primero hay que reabrirlo desde el formulario.
No. Un pedido, un episodio.
No hay límite declarado, pero conviene enviar de a uno y con una pausa mínima entre pedidos en las cargas masivas iniciales.
No. Los tres campos de TISS son los únicos opcionales del cuestionario. Si se envían, tienen que estar ordenados de mínimo a máximo.
Los cambios se publican en el propio cuestionario, y esta página los refleja automáticamente. Conviene releerlo periódicamente en lugar de dejar los códigos fijos en el código, sobre todo las listas de opciones.