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).
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:

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:
Especialidad | Plantilla |
---|---|
ALLERGY | Alergia |
ANESTHESIOLOGY | Preoperatorio |
CARDIOLOGY | Cardiología |
GASTROENTEROLOGY | Digestivo |
GYNECOLOGY | Ginecología |
HEMATOLOGY | Hematología |
INTERNAL_MEDICINE | Medicina interna |
MAXILLOFACIAL | Máxilofacial |
NEPHROLOGY | Nefrología |
NEUROLOGY | Neurología |
ONCOLOGY | Cáncer de mama |
OPHTHALMOLOGY | Oftalmología |
ORTHOPEDICS | Columna |
ORTHOPEDICS | Hombro |
ORTHOPEDICS | Mano |
ORTHOPEDICS | Pie |
ORTHOPEDICS | Rodilla |
ORTHOPEDICS | Cadera |
ORTHOPEDICS | Codo |
OTOLARYNGOLOGY | Otorrinolaringología |
PSYCHIATRY | Psiquiatría |
PULMONOLOGY | Neumología |
RHEUMATOLOGY | Reumatología |
SPORTS_MEDICINE | Reconocimiento deportivo |
VASCULAR | Vascular |
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.
4. Generar el link
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
:
token
: Este valor es el que recibes en el parrafo anterior.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 llavepatient
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 uncontext
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.
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
ocrypto.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:
- v1.1
- v1.0
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 desection
.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"
}
}
}
]
}
}
El formato de respuesta es basado en HL7 FHIR con la siguiente estructura:
Appointment
report
: información sobre el informe elegido por el doctor.context
: cualquier metadata adicional enviada por ti.contained
Patient
Practitioner
Bundle
: la lista de observaciones estructuradas.Observation
Observation
- ...
Observation
Composition
: Este es el informe final escrito por el doctor. Tiene una lista desection
.
Por ejemplo, el formato del request de Codoc de una consulta de hombro a tu webhook podría ser el siguiente:
{
"resourceType": "Appointment",
"contained": [
{
"resourceType": "Practitioner",
"identifier": [
{
"value": "John"
}
]
},
{
"resourceType": "Patient",
"identifier": [
{
"value": "1234"
}
]
},
{
"resourceType": "Bundle",
"type": "collection",
"entry": [
{
"resource": {
"resourceType": "Observation",
"status": "final",
"code": {
"coding": [
{
"system": "https://codoctech.com",
"code": "1",
"display": "Motivo de consulta"
}
]
},
"valueString": "Dolor en el hombro derecho"
}
},
{
"resource": {
"resourceType": "Observation",
"status": "final",
"code": {
"coding": [
{
"system": "https://codoctech.com",
"code": "129",
"display": "Lateralidad"
}
]
},
"valueString": "Derecha"
}
},
{
"resource": {
"resourceType": "Observation",
"status": "final",
"code": {
"coding": [
{
"system": "https://codoctech.com",
"code": "4",
"display": "Dominancia"
}
]
},
"valueString": "Zurdo"
}
},
{
"resource": {
"resourceType": "Observation",
"status": "final",
"code": {
"coding": [
{
"system": "https://codoctech.com",
"code": "136",
"display": "Infiltración corticoides"
}
]
},
"valueBoolean": false
}
},
{
"resource": {
"resourceType": "Observation",
"status": "final",
"code": {
"coding": [
{
"system": "https://codoctech.com",
"code": "138",
"display": "Ondas de choque previas"
}
]
},
"valueBoolean": false
}
},
{
"resource": {
"resourceType": "Observation",
"status": "final",
"code": {
"coding": [
{
"system": "https://codoctech.com",
"code": "137",
"display": "PRP previo"
}
]
},
"valueBoolean": false
}
}
]
},
{
"resourceType": "Composition",
"status": "final",
"type": {
"text": "Consultation"
},
"date": "2025-03-04T15:41:34.824623",
"author": [
{
"reference": "Doctor/John",
"display": "John"
}
],
"title": "Hombro",
"section": [
{
"title": "Motivo de consulta",
"text": {
"status": "generated",
"div": "Se presenta dolor en el hombro derecho."
}
},
{
"title": "Anamnesis",
"text": {
"status": "generated",
"div": "Se identifica dominancia zurda en el paciente, lo que puede influir en la actividad del hombro derecho."
}
},
{
"title": "Tratamientos previos",
"text": {
"status": "generated",
"div": "Se reporta que ha recibido infiltraciones de corticoides, mientras que no se han realizado tratamientos con PRP ni ondas de choque en el pasado."
}
},
{
"title": "Exploración",
"text": {
"status": "generated",
"div": ""
}
},
{
"title": "Resultados de imagen",
"text": {
"status": "generated",
"div": ""
}
},
{
"title": "Resultados laboratorio",
"text": {
"status": "generated",
"div": ""
}
},
{
"title": "Impresión diagnóstica",
"text": {
"status": "generated",
"div": ""
}
},
{
"title": "Plan",
"text": {
"status": "generated",
"div": ""
}
},
{
"title": "Pruebas de imagen solicitadas",
"text": {
"status": "generated",
"div": ""
}
}
]
}
],
"status": "fulfilled",
"specialty": [
{
"coding": [
{
"system": "http://snomed.info/sct",
"code": "394801008",
"display": "Trauma & orthopaedics"
}
]
}
],
"description": "Hombro",
"start": "2025-03-04T14:41:10.910425Z",
"end": "2025-03-04T14:41:34.734163Z",
"participant": [
{
"actor": {
"reference": "Patient/1234",
"display": "Maribel Criado"
},
"status": "accepted"
},
{
"actor": {
"reference": "Doctor/John",
"display": "John"
},
"status": "accepted"
}
],
"context": {
"consultation_id": 4,
"doctor_id": 4
},
"report": {
"id": 3,
"title": "Hombro"
}
}