Saltar al contenido principal

Integración

Introducción

Codoc se comunica con el HIS a través de un webhook, una URL proporcionada por la aplicación que va a recibir la notificación. Cuando el doctor guarda una consulta, Codoc hace una solicitud HTTP POST a esta URL con los datos extraídos de la consulta. Esto permite que la aplicación receptora procese la información recibida de manera automática y tome las acciones correspondientes (guardar los datos de la consulta, pedir una cita, una resonancia magnética, etc).

Integration flow

Pasos a seguir

1. Darse de alta

El primer paso es darse de alta con nuestro equipo. Si no estás seguro o quieres darte de alta contacta con nuestro equipo en [email protected].

Una vez estés dado de alta entra en el portal de administración, ve a Ajustes y genera una Clave API:

tip

Puedes generar hasta 3 claves API. Asegurate de guardar tus claves en un sitio seguro, y recomendamos rotar las claves con relativa frecuencia.

2. Crear doctores

Para que un doctor pueda utilizar Codoc tiene que estar vinculado con un usuario y tiene que estar activo. Para cada uno de los doctores que quieres activar, envía un POST request a https://api.codoctech.com/v1/doctors para crearlo, y utiliza la clave API para autenticarte mediante el header X-API-Key del request.

X-API-Key: <MI_CLAVE_API>

Por ejemplo, para dar de alta al doctor Juan García enviarías un POST con:

{
"id": "1",
"name": "Juan García",
"specialties": ["ORTHOPEDICS", "SPORTS_MEDICINE", "CARDIOLOGY"]
}

El parámetro id es el ID interno que utilizáis en vuestro HIS. Es obligatorio proporcionarlo para poder ver qué doctores están haciendo consultas. El id debe ser un string alfanumérico, así que si utilizáis números, conviértelo en un string antes de crear el doctor. El parámetro specialties es una lista, y se utiliza para dar acceso a las plantillas de esa especialidad. En el ejemplo de arriba, el doctor García tendría acceso a todas las plantillas de traumatología, medicina deportiva, y cardiología. Aquí tienes la lista de especialidades y sus plantillas:

EspecialidadPlantilla
ALLERGYAlergia
ANESTHESIOLOGYPreoperatorio
CARDIOLOGYCardiología
GASTROENTEROLOGYDigestivo
GYNECOLOGYGinecología
HEMATOLOGYHematología
INTERNAL_MEDICINEMedicina interna
MAXILLOFACIALMáxilofacial
NEPHROLOGYNefrología
NEUROLOGYNeurología
ONCOLOGYCáncer de mama
OPHTHALMOLOGYOftalmología
ORTHOPEDICSColumna
ORTHOPEDICSHombro
ORTHOPEDICSMano
ORTHOPEDICSPie
ORTHOPEDICSRodilla
ORTHOPEDICSCadera
ORTHOPEDICSCodo
OTOLARYNGOLOGYOtorrinolaringología
PSYCHIATRYPsiquiatría
PULMONOLOGYNeumología
RHEUMATOLOGYReumatología
SPORTS_MEDICINEReconocimiento deportivo
VASCULARVascular

3. Detallar el webhook

Ve de nuevo a Ajustes > Integración, y rellena el campo detallando el endpoint para tu webhook. Rellena también el secreto webhook (al menos 16 caracteres).

¿Qué es el secreto webhook?

Para asegurarte de que tu servidor solo procese las notificaciones de webhook que fueron enviadas por Codoc y para asegurar que la entrega no haya sido alterada, deberías validar la firma del webhook antes de procesar la consulta. Para ello, añade un SECRETO de al menos 16 caracteres que utilizarás para validar la entrega.

En la sección 5 puedes ver un ejemplo de como verificar un informe.

Los doctores entran al UI de Codoc a través de un link que genera el HIS. Para generar un link, envía un POST request a /auth/generate-token con el id interno del doctor:

{
"doctor_id": "1"
}

Nota como el ID del doctor es el mismo que el que hemos creado antes. Recibirás un token del POST que permitirá al doctor entrar al UI de Codoc. Este token tiene una validez de 10 minutos.

{
"token": "qKaIW2jUWEFT841tP_vMYoswTAXiejtPgqf3sHQ90iY"
}

El link final consiste en dos partes, el token y el context:

  1. token: Este valor es el que recibes en el parrafo anterior.
  2. context: Este valor añade contexto sobre la consulta (principalmente información sobre el paciente). El formato enviado es un JSON codificado en base64 seguras para URL. El JSON deberá tener la llave patient con información sobre el paciente. Opcionalmente, podrás pasar contexto adicional que Codoc te devolverá en el hook sin modificar. Por ejemplo, el siguiente ejemplo sería un context válido:
{
"patient": {
"resourceType": "Patient",
"identifier": [
{
"value": "123456"
}
],
"name": [{"use": "official", "text": "John Smith"}]
},
"is_first_visit": true,
"consultation_id": 123,
"any_other_key": "more metadata information"
}

Con ese ejemplo el URL final sería

https://consulta.codoctech.com/?token=qKaIW2jUWEFT841tP_vMYoswTAXiejtPgqf3sHQ90iY&context=eyJwYXRpZW50IjogeyJyZXNvdXJjZVR5cGUiOiAiUGF0aWVudCIsICJpZGVudGlmaWVyIjogW3sidmFsdWUiOiAiMTIzNDU2In1dLCAibmFtZSI6IFt7InVzZSI6ICJvZmZpY2lhbCIsICJ0ZXh0IjogIkpvaG4gU21pdGgifV19LCAiaXNfZmlyc3RfdmlzaXQiOiB0cnVlLCAiY29uc3VsdGF0aW9uX2lkIjogMTIzLCAiYW55X290aGVyX2tleSI6ICJtb3JlIG1ldGFkYXRhIGluZm9ybWF0aW9uIn0=

Cualquier información extra será recopilada en la llave context dentro del recurso Appointment. Para ver un ejemplo refiere a la sección de Formato.

danger

Sin un contexto no puedes guardar una consulta (Codoc necesita información sobre el paciente), así que asegúrate de pasar el context en el URL.

Ejemplo

Si enlazamos lo que llevamos hasta ahora, en Python tendríamos lo siguiente:

import json
from urllib.parse import urlencode
from base64 import urlsafe_b64encode

import requests


CONSULTATIONS_URL = "https://consulta.codoctech.com"
API_KEY = "2f32c3d709ff7db4ab43143ea477919d"
API_URL = "https://api.codoctech.com/v1"

def create_doctor(uid, name):
res = requests.post(
f"{API_URL}/doctors",
json={"id": str(uid), "name": name, "specialty": "Traumatología"},
headers={"x-api-key": f"{API_KEY}"},
)
if res.status_code == 201:
print("¡Doctor creado!")


def get_ephemeral_token(doctor_id: str):
res = requests.post(
f"{API_URL}/auth/generate-token",
json={"doctor_id": doctor_id},
headers={"X-API-Key": API_KEY},
)
return res.json()


def generate_consultation_url(doctor_id: str, context):
query_params = {
"token": get_ephemeral_token(doctor_id)["token"],
"context": urlsafe_b64encode(json.dumps(context).encode()),
}
return f"{CONSULTATIONS_URL}?{urlencode(query_params)}"

5. Recibir los informes

Codoc creará un POST request a tu webhook cada vez que el doctor genere el informe. Se intentará enviar la POST request dos veces, desistiendo si el código de respuesta del HIS no es de 200.

Codoc utilizará tu token SECRETO para crear una firma de hash que se te enviará con cada informe. La firma de hash aparecerá en cada request como el valor del header X-Signature-256.

Una vez recibas el POST request, lo primero se debe hacer es comprobar que ha sido Codoc quien ha envíado el informe y no otro agente. Compara el hash que Codoc envió con el hash esperado que calculaste y asegúrate de que coincidan.

Hay algunas cosas importantes que debes tener en cuenta al validar las cargas útiles de webhook:

  • Codoc utiliza un digest hexadecimal HMAC para calcular el hash.
  • La firma de hash siempre comienza con sha256=.
  • La firma de hash se genera utilizando el token secreto de tu webhook y el payload del request.
  • Si tu lenguaje e implementación de servidor especifica una codificación de caracteres, asegúrate de manejar el request como UTF-8.
  • Nunca uses un operador == simple. En su lugar, considera usar un método como secure_compare o crypto.timingSafeEqual, que realiza una comparación de cadenas "en tiempo constante" para ayudar a mitigar ciertos ataques de tiempo contra operadores de igualdad regulares o bucles regulares en lenguajes optimizados por JIT.

Ejemplo

En Python y Django podríamos escribir nuestro webhook de una manera muy sencilla:

import hmac
import json
from hashlib import sha256

from django.http import JsonResponse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.csrf import csrf_exempt

from his.models import Consultation

@method_decorator(csrf_exempt, name="dispatch")
class ConsultationWebhookView(View):
TOKEN = "MI-SECRETO"

def post(self, request, *args, **kwargs):
received_signature = request.headers.get("x-signature-256")
expected_signature = f"sha256={hmac.new(self.TOKEN.encode(), msg=request.body, digestmod=sha256).hexdigest()}"
is_valid = hmac.compare_digest(received_signature, expected_signature)
if is_valid:
data = json.loads(request.body)
cons = Consultation(data=data)
cons.save()
return JsonResponse(
{"status": "success", "message": "Consultation received"},
status=200,
)
else:
print(f"¡No valido!")

Formato

Dependiendo de la versión, tenemos el siguiente formato de respuesta HL7 FHIR:

El formato de respuesta es basado en HL7 FHIR con la siguiente estructura:

  • context: cualquier metadata adicional enviada por ti.
  • fhir:
    • Bundle
      • Patient
      • Practitioner
      • Composition: Este es el informe final escrito por el doctor. Tiene una lista de section.
      • Observation
      • Observation
      • ...
      • Observation
      • MedicationRequest
      • ServiceRequest

Por ejemplo, el formato del request de Codoc de una consulta de hombro a tu webhook podría ser el siguiente:

{
"context": {
"additional_metadata": 4,
"is_first_time": true
},
"fhir": {
"resourceType": "Bundle",
"id": "bundle-consultation-example",
"type": "document",
"timestamp": "2024-01-11T10:00:00Z",
"entry": [
{
"fullUrl": "urn:uuid:composition-1",
"resource": {
"resourceType": "Composition",
"id": "composition-1",
"status": "final",
"subject": {
"reference": "Patient/patient-1"
},
"date": "2024-01-11T09:30:00Z",
"author": [
{
"reference": "Practitioner/practitioner-1"
}
],
"title": "Consultation Note",
"section": [
{
"title": "Vitals and Initial Observations",
"text": "Vitals are correct with emphasis on...",
"entry": [
{
"reference": "Observation/obs-1"
},
{
"reference": "Observation/obs-2"
}
]
},
{
"title": "Findings and Plan",
"text": "The plan is to...",
"entry": [
{
"reference": "Observation/obs-3"
},
{
"reference": "MedicationRequest/medrequest-1"
},
{
"reference": "ServiceRequest/srvrequest-1"
}
]
}
]
}
},
{
"fullUrl": "urn:uuid:patient-1",
"resource": {
"resourceType": "Patient",
"id": "patient-1",
"identifier": [
{
"system": "http://examplehospital.org/patients",
"value": "123456"
}
],
"name": [
{
"use": "official",
"family": "Doe",
"given": [
"John"
]
}
]
}
},
{
"fullUrl": "urn:uuid:practitioner-1",
"resource": {
"resourceType": "Practitioner",
"id": "practitioner-1",
"identifier": [
{
"system": "http://examplehospital.org/providers",
"value": "DR987"
}
],
"name": [
{
"use": "official",
"family": "Smith",
"given": [
"Jane"
]
}
]
}
},
{
"fullUrl": "urn:uuid:obs-1",
"resource": {
"resourceType": "Observation",
"id": "obs-1",
"status": "final",
"category": [
{
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/observation-category",
"code": "vital-signs"
}
]
}
],
"code": {
"coding": [
{
"system": "http://loinc.org",
"code": "85354-9",
"display": "Blood pressure panel"
}
]
},
"subject": {
"reference": "Patient/patient-1"
},
"effectiveDateTime": "2024-01-11T09:20:00Z",
"valueString": "Blood Pressure: 120/80 mmHg"
}
},
{
"fullUrl": "urn:uuid:obs-2",
"resource": {
"resourceType": "Observation",
"id": "obs-2",
"status": "final",
"category": [
{
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/observation-category",
"code": "laboratory"
}
]
}
],
"code": {
"coding": [
{
"system": "http://loinc.org",
"code": "718-7",
"display": "Hemoglobin [Mass/volume] in Blood"
}
]
},
"subject": {
"reference": "Patient/patient-1"
},
"effectiveDateTime": "2024-01-11T09:25:00Z",
"valueQuantity": {
"value": 14.5,
"unit": "g/dL",
"system": "http://unitsofmeasure.org",
"code": "g/dL"
}
}
},
{
"fullUrl": "urn:uuid:obs-3",
"resource": {
"resourceType": "Observation",
"id": "obs-3",
"status": "final",
"category": [
{
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/observation-category",
"code": "clinical"
}
]
}
],
"code": {
"coding": [
{
"system": "http://snomed.info/sct",
"code": "281798007",
"display": "Physical examination finding"
}
],
"text": "General physical exam"
},
"subject": {
"reference": "Patient/patient-1"
},
"effectiveDateTime": "2024-01-11T09:28:00Z",
"valueString": "Patient reports mild headache and slight dizziness."
}
},
{
"fullUrl": "urn:uuid:medrequest-1",
"resource": {
"resourceType": "MedicationRequest",
"id": "medrequest-1",
"status": "active",
"intent": "order",
"medicationCodeableConcept": {
"coding": [
{
"system": "http://snomed.info/sct",
"code": "387517004",
"display": "Ibuprofen 200mg oral tablet"
}
],
"text": "Ibuprofen 200mg Tablet"
},
"subject": {
"reference": "Patient/patient-1"
},
"authoredOn": "2024-01-11T09:45:00Z",
"requester": {
"reference": "Practitioner/practitioner-1"
},
"dosageInstruction": [
{
"text": "Take 1 tablet by mouth every 6 hours as needed for pain",
"timing": {
"repeat": {
"frequency": 1,
"period": 6,
"periodUnit": "h"
}
},
"route": {
"coding": [
{
"system": "http://snomed.info/sct",
"code": "26643006",
"display": "Oral route"
}
]
}
}
]
}
},
{
"fullUrl": "urn:uuid:srvrequest-1",
"resource": {
"resourceType": "ServiceRequest",
"id": "srvrequest-1",
"status": "active",
"intent": "order",
"code": {
"coding": [
{
"system": "http://snomed.info/sct",
"code": "306006008",
"display": "Magnetic resonance imaging of brain"
}
],
"text": "MRI of the Brain"
},
"subject": {
"reference": "Patient/patient-1"
},
"authoredOn": "2024-01-11T09:50:00Z",
"requester": {
"reference": "Practitioner/practitioner-1"
}
}
}
]
}
}