API y webhooks de Doctia
API REST v1 · JSON · https://doctia.co/api/v1
Qué ofrece la API
La API de Doctia conecta el consultorio con sus otros sistemas: lee y escribe pacientes y citas, lee las historias clínicas y evoluciones con sus diagnósticos, los paquetes de sesiones y el catálogo de profesionales, servicios y sedes. Los webhooks avisan en el momento en que algo pasa: una cita nueva, una cita atendida o cancelada, una historia firmada, un paciente nuevo o una sesión firmada.
Sirve para la contabilidad, la facturación electrónica, los tableros de BI, el CRM y la logística de atención domiciliaria. Y con la exportación completa, los datos de su consultorio son suyos: los descarga cuando quiera, en CSV y JSON, también si la suscripción está vencida.
Cómo obtener una clave
- En el panel de Doctia, abra RIPS y API › API y webhooks.
- Solo el titular de la cuenta (rol propietario) crea y revoca claves, en los planes PRO y CENTRO.
- Póngale un nombre (por ejemplo «Facturación») y marque solo los permisos que esa integración necesita.
- La clave completa (
dk_live_…) se muestra una sola vez: cópiela y guárdela en el gestor de secretos de su servidor. Doctia guarda solo su huella, no la clave. - Si una clave se expone, revóquela en el mismo lugar: deja de funcionar en el acto.
Autenticación
Cada solicitud lleva la clave en el encabezado Authorization:
curl -H "Authorization: Bearer $DOCTIA_CLAVE" \
"https://doctia.co/api/v1/cuenta"La API es de servidor a servidor: nunca ponga la clave en una página web ni en una aplicación móvil. Cada clave ve solo su cuenta; un id de otra cuenta responde 404, igual que uno que no existe. Cada llamada queda en la bitácora de la cuenta, con la clave que la hizo.
Permisos
Una operación sin el permiso responde 403 PERMISO_INSUFICIENTE.
| Permiso | Qué permite |
|---|---|
pacientes:leer | Lista y datos de los pacientes. |
pacientes:escribir | Registrar pacientes y cambiar sus datos. |
citas:leer | Agenda: citas por fecha, profesional y estado. |
citas:escribir | Agendar, reprogramar, cambiar el estado y cancelar, con las mismas reglas de la agenda. |
historias:leer | Historias y evoluciones (firmadas y borradores) con sus diagnósticos. |
historias:escribir | Crear el borrador de una evolución. La firma siempre la hace el profesional en el panel. |
paquetes:leer | Paquetes de sesiones con el estado y la firma de cada sesión. |
catalogo:leer | Profesionales, servicios y sedes. |
exportar | Descargar un ZIP con todos los datos de la cuenta (incluidas las historias). |
Límites
Hasta 120 solicitudes por minuto por clave. Cada respuesta trae X-RateLimit-Limit, X-RateLimit-Remaining y X-RateLimit-Reset (segundos epoch en que se reinicia la ventana). Al pasarse, la respuesta es 429 con Retry-After. El cuerpo de una solicitud puede pesar hasta 1 MB.
Paginación
Las listas de pacientes, citas, historias y paquetes van por cursor, ordenadas por id: limite (de 1 a 200; 50 si no lo envía) y despues_de (el id del último elemento recibido). Para la página siguiente envíe despues_de con el valor de paginacion.siguiente.
{
"data": [ { "id": 9123, "estado": "confirmada", "fecha": "2026-10-20", … } ],
"paginacion": { "limite": 50, "hay_mas": true, "siguiente": 9123 }
}Fechas y zona horaria
- Los instantes (
creada_en,actualizada_en,firmada_en…) van en ISO 8601 en UTC, con «Z». - La agenda (
fecha,hora_inicio,hora_fin) va en hora de Colombia, sin zona, igual que en el panel.inicioyfintraen el mismo momento con el desfase de Colombia (-05:00, sin horario de verano). - Los filtros
desdeyhastason fechas de Colombia (AAAA-MM-DD);actualizados_desdeyactualizadas_desdeson instantes ISO 8601.
{
"id": 9123,
"estado": "confirmada",
"fecha": "2026-10-20",
"hora_inicio": "09:00",
"hora_fin": "09:30",
"inicio": "2026-10-20T09:00:00-05:00",
"fin": "2026-10-20T09:30:00-05:00",
"paciente": { "id": 6538, "nombre": "Laura Gómez", "telefono": "3001234567", "correo": null },
"profesional": { "id": 12, "nombre": "Ana Ruiz" },
"servicio": { "id": 40, "nombre": "Consulta" },
"sede": null,
"creada_en": "2026-10-08T19:17:39Z",
"actualizada_en": "2026-10-08T19:20:02Z"
}Errores
Todo error tiene la misma forma; programe contra code, que no cambia:
HTTP/1.1 409 Conflict
{
"error": {
"code": "CHOQUE",
"message": "Ese horario se cruza con otra cita (de 09:00 a 09:30). Elija otra hora."
}
}| HTTP | Códigos | Cuándo |
|---|---|---|
400 | SOLICITUD_INVALIDA, PARAMETRO_INVALIDO, CAMPO_REQUERIDO, CAMPO_DESCONOCIDO, VALOR_INVALIDO, JSON_INVALIDO, UN_CAMBIO_A_LA_VEZ | El cuerpo o un filtro no sirve; «campo» dice cuál. |
400 | FUERA_DE_HORARIO, DESCANSO, BLOQUEO, FESTIVO, NO_ATIENDE, FECHA_PASADA_SIN_MOTIVO, NO_SE_MUEVE | La agenda no admite esa hora (las mismas reglas del panel). |
401 | NO_AUTENTICADO, CLAVE_INVALIDA, CLAVE_REVOCADA, CLAVE_SIN_TITULAR | Falta la clave, no existe, fue revocada o quien la creó ya no es titular. |
402 | CUENTA_VENCIDA_SOLO_LECTURA | La suscripción está vencida o cancelada: leer y exportar siguen abiertos. |
403 | PERMISO_INSUFICIENTE, PLAN_SIN_API, CUENTA_ARCHIVADA, FIRMA_SOLO_EN_PANEL | La clave no tiene ese permiso, o la operación no está disponible por la API. |
404 | NO_ENCONTRADO, PACIENTE_NO_ENCONTRADO, CITA_NO_ENCONTRADA, PROFESIONAL_NO_ENCONTRADO, RUTA_NO_ENCONTRADA | No existe en esta cuenta. Un id de otra cuenta responde igual. |
409 | CHOQUE, PACIENTE_EXISTE, BORRADOR_EXISTENTE, PAGO_EN_CURSO, EXPORTACION_EN_CURSO, EXPORTACION_EN_PROCESO | Conflicto con el estado actual; en un duplicado, «existente_id» trae el id del que ya existe. |
410 | EXPORTACION_VENCIDA | El archivo de la exportación ya no está (dura 7 días). |
413 | CUERPO_DEMASIADO_GRANDE | El cuerpo pasa de 1 MB. |
429 | DEMASIADAS_SOLICITUDES, DEMASIADOS_INTENTOS, DEMASIADAS_EXPORTACIONES | Pasó un límite; espere lo que diga Retry-After. |
500 | ERROR_INTERNO | Error de Doctia. Reintente; si sigue, escríbanos con la hora de la solicitud. |
Endpoints
Base: https://doctia.co/api/v1. Todos responden JSON: una lista va en data (arreglo, con paginacion) y un elemento, en data (objeto). En los ejemplos, $DOCTIA_CLAVE es su clave.
Cuenta
GET/cuentasin permiso
La cuenta, la clave y sus permisos. Úselo para probar la clave.
curl -H "Authorization: Bearer $DOCTIA_CLAVE" \
"https://doctia.co/api/v1/cuenta"Pacientes
GET/pacientespacientes:leer
Lista paginada. Filtros: actualizados_desde (instante ISO), documento, tipo_documento.
curl -H "Authorization: Bearer $DOCTIA_CLAVE" \
"https://doctia.co/api/v1/pacientes?documento=1053773323"GET/pacientes/{id}pacientes:leer
Un paciente.
curl -H "Authorization: Bearer $DOCTIA_CLAVE" \
"https://doctia.co/api/v1/pacientes/6538"POST/pacientespacientes:escribir
Crea un paciente. Obligatorios: tipo_documento, numero_documento, nombres y apellidos. Un documento repetido da 409 PACIENTE_EXISTE con existente_id.
curl -X POST -H "Authorization: Bearer $DOCTIA_CLAVE" \
-H "Content-Type: application/json" \
-d '{"tipo_documento":"CC","numero_documento":"1053773323","nombres":"Laura","apellidos":"Gómez","telefono":"3001234567"}' \
"https://doctia.co/api/v1/pacientes"PATCH/pacientes/{id}pacientes:escribir
Cambia solo los campos enviados (null borra el valor).
curl -X PATCH -H "Authorization: Bearer $DOCTIA_CLAVE" \
-H "Content-Type: application/json" \
-d '{"correo":"laura@ejemplo.co","eps":"Sura"}' \
"https://doctia.co/api/v1/pacientes/6538"Citas
GET/citascitas:leer
Lista paginada. Filtros: desde y hasta (fechas de Colombia), profesional_id, paciente_id, estado (varios separados por coma) y actualizadas_desde.
curl -H "Authorization: Bearer $DOCTIA_CLAVE" \
"https://doctia.co/api/v1/citas?desde=2026-10-20&hasta=2026-10-24&estado=pendiente,confirmada"GET/citas/{id}citas:leer
Una cita.
curl -H "Authorization: Bearer $DOCTIA_CLAVE" \
"https://doctia.co/api/v1/citas/9123"POST/citascitas:escribir
Agenda con las reglas de la agenda del panel: jornada, descanso, bloqueos, festivos y sin cruces (409 CHOQUE). La duración sale del servicio. Por la API no se agenda en sobrecupo.
curl -X POST -H "Authorization: Bearer $DOCTIA_CLAVE" \
-H "Content-Type: application/json" \
-d '{"paciente_id":6538,"profesional_id":12,"servicio_id":40,"fecha":"2026-10-20","hora":"09:00"}' \
"https://doctia.co/api/v1/citas"PATCH/citas/{id}citas:escribir
Cambia el estado (pendiente, confirmada, atendida, cancelada, no_asistio) o reprograma (fecha, hora, profesional_id). Uno de los dos por solicitud.
curl -X PATCH -H "Authorization: Bearer $DOCTIA_CLAVE" \
-H "Content-Type: application/json" \
-d '{"fecha":"2026-10-21","hora":"10:30"}' \
"https://doctia.co/api/v1/citas/9123"POST/citas/{id}/cancelarcitas:escribir
Cancela. Motivos: paciente_cancelo, reprogramada, no_confirmo, salud_fuerza_mayor, costo, consultorio, error_agenda u otro (con «detalle»). Algunas cuentas exigen el motivo.
curl -X POST -H "Authorization: Bearer $DOCTIA_CLAVE" \
-H "Content-Type: application/json" \
-d '{"motivo":"paciente_cancelo"}' \
"https://doctia.co/api/v1/citas/9123/cancelar"Historias clínicas y evoluciones
GET/historiashistorias:leer
Firmadas y borradores, con contenido y diagnósticos CIE-10. Filtros: paciente_id, profesional_id, estado (borrador o firmada), desde, hasta, actualizadas_desde.
curl -H "Authorization: Bearer $DOCTIA_CLAVE" \
"https://doctia.co/api/v1/historias?estado=firmada&actualizadas_desde=2026-10-01T00:00:00Z"GET/historias/{id}historias:leer
Una historia con sus notas aclaratorias y su sello SHA-256.
curl -H "Authorization: Bearer $DOCTIA_CLAVE" \
"https://doctia.co/api/v1/historias/2210"POST/historiashistorias:escribir
Crea el BORRADOR de una evolución a nombre del profesional que la firmará. La firma solo se hace en el panel (403 FIRMA_SOLO_EN_PANEL).
curl -X POST -H "Authorization: Bearer $DOCTIA_CLAVE" \
-H "Content-Type: application/json" \
-d '{"paciente_id":6538,"cita_id":9123,"analisis":"Evolución favorable.","diagnosticos":[{"codigo":"F900"}]}' \
"https://doctia.co/api/v1/historias"Paquetes y sesiones
GET/paquetespaquetes:leer
Paquetes con sus líneas y cada sesión: estado de la cita y de la firma de asistencia. Filtros: paciente_id, estado.
curl -H "Authorization: Bearer $DOCTIA_CLAVE" \
"https://doctia.co/api/v1/paquetes?paciente_id=6538"GET/paquetes/{id}paquetes:leer
Un paquete.
curl -H "Authorization: Bearer $DOCTIA_CLAVE" \
"https://doctia.co/api/v1/paquetes/31"Profesionales, servicios y sedes
GET/profesionalescatalogo:leer
Activos (activos=todos incluye los inactivos), con los servicios de cada uno.
curl -H "Authorization: Bearer $DOCTIA_CLAVE" \
"https://doctia.co/api/v1/profesionales"GET/profesionales/{id}catalogo:leer
Un profesional.
curl -H "Authorization: Bearer $DOCTIA_CLAVE" \
"https://doctia.co/api/v1/profesionales/12"GET/servicioscatalogo:leer
Servicios con duración, precio y profesionales.
curl -H "Authorization: Bearer $DOCTIA_CLAVE" \
"https://doctia.co/api/v1/servicios"GET/servicios/{id}catalogo:leer
Un servicio.
curl -H "Authorization: Bearer $DOCTIA_CLAVE" \
"https://doctia.co/api/v1/servicios/40"GET/sedescatalogo:leer
Sedes del consultorio.
curl -H "Authorization: Bearer $DOCTIA_CLAVE" \
"https://doctia.co/api/v1/sedes"GET/sedes/{id}catalogo:leer
Una sede.
curl -H "Authorization: Bearer $DOCTIA_CLAVE" \
"https://doctia.co/api/v1/sedes/3"Exportación
POST/exportacionesexportar
Crea un trabajo asíncrono (202). Una en curso por cuenta y hasta 10 cada 24 horas.
curl -H "Authorization: Bearer $DOCTIA_CLAVE" -X POST \
"https://doctia.co/api/v1/exportaciones"GET/exportacionesexportar
Las últimas 20 exportaciones.
curl -H "Authorization: Bearer $DOCTIA_CLAVE" \
"https://doctia.co/api/v1/exportaciones"GET/exportaciones/{id}exportar
Estado: pendiente, procesando, lista, fallida o vencida.
curl -H "Authorization: Bearer $DOCTIA_CLAVE" \
"https://doctia.co/api/v1/exportaciones/exp_3f9a0c1b2d4e5f60718293a4"GET/exportaciones/{id}/archivoexportar
El ZIP, cuando está lista.
curl -H "Authorization: Bearer $DOCTIA_CLAVE" \
"https://doctia.co/api/v1/exportaciones/exp_3f9a0c1b2d4e5f60718293a4/archivo" \
-o doctia.zipWebhooks
En RIPS y API › API y webhooks el titular registra una URL https:// y elige los eventos. Doctia envía un POST con JSON en cuanto el evento ocurre, sin demorar ni frenar la acción en el panel. Al crear el webhook se muestra su secreto (whsec_…) una sola vez; con él verifica la firma. El botón «Enviar prueba» manda un evento prueba y muestra la respuesta de su servidor.
| Evento | Cuándo | datos |
|---|---|---|
cita.creada | Se agenda una cita (panel, página pública, API o pago en línea confirmado). | { cita, estado_anterior } |
cita.actualizada | Cambia la fecha, la hora, el profesional, el servicio o el estado (confirmada, no asistió…). | { cita, estado_anterior } |
cita.atendida | La cita se marca como atendida. | { cita, estado_anterior } |
cita.cancelada | La cita se cancela. | { cita, estado_anterior } |
historia.firmada | El profesional firma una historia o evolución. Lleva el resumen sin contenido clínico. | { historia } |
paciente.creado | Se registra un paciente nuevo. | { paciente } |
sesion.firmada | El paciente firma la asistencia a una sesión. | { sesion } |
Encabezados
X-Doctia-Firma:sha256=seguido del HMAC-SHA256 en hexadecimal del cuerpo exacto, con el secreto del webhook.X-Doctia-Evento: el nombre del evento.X-Doctia-Evento-Id: id del evento, igual en todos los reintentos (para no procesarlo dos veces).X-Doctia-Entrega: id de la entrega.
POST /su/ruta HTTP/1.1
Content-Type: application/json; charset=utf-8
User-Agent: Doctia-Webhooks/1.0 (+https://doctia.co/desarrolladores)
X-Doctia-Evento: cita.creada
X-Doctia-Evento-Id: evt_5b1e0c2a9f3d4e7a8b6c1d2e3f4a5b6c
X-Doctia-Entrega: 4812
X-Doctia-Firma: sha256=3f1c…
{
"id": "evt_5b1e0c2a9f3d4e7a8b6c1d2e3f4a5b6c",
"evento": "cita.creada",
"creado_en": "2026-10-08T19:17:39Z",
"cuenta_id": 65,
"prueba": false,
"datos": {
"cita": { "id": 9123, "estado": "pendiente", "fecha": "2026-10-20", "hora_inicio": "09:00", … },
"estado_anterior": null
}
}Entrega y reintentos
Una respuesta 2xx en menos de 10 segundos la da por entregada. Si no, Doctia reintenta a los 1 min, 5 min, 30 min, 2 h, 6 h y 12 h (7 intentos en total) con el mismo cuerpo; no sigue redirecciones. En el panel se ven las últimas entregas de cada webhook con su resultado. Los eventos de cita llevan la cita tal como quedó en ese momento; la historia firmada llega sin contenido clínico: consúltelo con GET /historias/{id} y el permiso historias:leer.
Verificar la firma en Node.js
// Node.js con Express: valide la firma sobre el cuerpo EXACTO (sin volver a serializarlo).
const crypto = require('crypto');
const express = require('express');
const app = express();
app.post('/doctia/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const esperada = 'sha256=' + crypto
.createHmac('sha256', process.env.DOCTIA_WEBHOOK_SECRETO)
.update(req.body) // Buffer con los bytes recibidos
.digest('hex');
const recibida = req.get('X-Doctia-Firma') || '';
const valida = recibida.length === esperada.length
&& crypto.timingSafeEqual(Buffer.from(recibida), Buffer.from(esperada));
if (!valida) return res.status(401).end();
const evento = JSON.parse(req.body.toString('utf8'));
// Guarde evento.id: si llega otra vez (reintento), no lo procese dos veces.
// Responda rápido (menos de 10 s) y haga el trabajo pesado en segundo plano.
res.status(200).end();
});Verificar la firma en Python
# Python con Flask: compare con hmac.compare_digest (tiempo constante).
import hashlib, hmac, json, os
from flask import Flask, abort, request
app = Flask(__name__)
SECRETO = os.environ["DOCTIA_WEBHOOK_SECRETO"].encode()
@app.post("/doctia/webhook")
def recibir():
cuerpo = request.get_data() # bytes exactos recibidos
esperada = "sha256=" + hmac.new(SECRETO, cuerpo, hashlib.sha256).hexdigest()
if not hmac.compare_digest(esperada, request.headers.get("X-Doctia-Firma", "")):
abort(401)
evento = json.loads(cuerpo)
# evento["id"] es el mismo en los reintentos: úselo para no repetir el trabajo.
return "", 200Exportación completa
Un ZIP con LEEME.txt y, por cada entidad (pacientes, citas, historias, notas aclaratorias, paquetes, sesiones, profesionales, servicios y sedes), un archivo JSON con la misma forma de la API y un CSV en UTF-8 listo para Excel. Es asíncrona, funciona también con la cuenta en solo lectura y cada archivo dura 7 días. Contiene datos de salud: guárdelo en un lugar seguro.
# 1. Crear el trabajo
curl -X POST -H "Authorization: Bearer $DOCTIA_CLAVE" https://doctia.co/api/v1/exportaciones
# → 202 { "data": { "id": "exp_3f9a…", "estado": "pendiente", … } }
# 2. Consultar hasta que el estado sea "lista"
curl -H "Authorization: Bearer $DOCTIA_CLAVE" https://doctia.co/api/v1/exportaciones/exp_3f9a…
# 3. Descargar el ZIP (dura 7 días)
curl -H "Authorization: Bearer $DOCTIA_CLAVE" -o doctia.zip \
https://doctia.co/api/v1/exportaciones/exp_3f9a…/archivoGuías de integración
Contabilidad
- Suscriba
cita.atendidapara registrar el ingreso de cada consulta. - Use
GET /serviciospara el precio de cada servicio yGET /citas?estado=atendida&desde=…&hasta=…para cuadrar el mes. - Para una conciliación completa, la exportación trae citas, servicios y paquetes con su valor.
Facturación electrónica
- Doctia no factura: su sistema de facturación emite la factura electrónica.
- Con
cita.atendidasabe qué consulta facturar; conhistoria.firmadasabe que la atención ya está cerrada. - Consulte
GET /historias/{id}para los diagnósticos CIE-10 y la clasificación RIPS (CUPS, finalidad, causa externa), yGET /pacientes/{id}para el documento y los datos del paciente. - El número de la factura se escribe luego en Doctia (RIPS del mes) para generar el RIPS por factura.
BI y tableros
- Carga inicial con la exportación completa (CSV o JSON).
- Luego, sincronización incremental cada cierto tiempo con
actualizadas_desdeen citas e historias yactualizados_desdeen pacientes, recorriendo las páginas condespues_de. Guarde el instante de cada corrida para la siguiente. - Respete el límite de 120 solicitudes por minuto: 200 elementos por página alcanzan para miles de registros en pocas solicitudes.
CRM
paciente.creadocrea el contacto;cita.creada,cita.atendida,cita.canceladaycita.actualizada(no asistió) alimentan el historial y las campañas de seguimiento.- Para agendar desde el CRM:
POST /pacientes(o búsquelo conGET /pacientes?documento=…) y luegoPOST /citas. Si la hora ya no está libre, la respuesta es 409CHOQUEy no se crea nada.
Logística de atención domiciliaria
- La ruta del día de cada profesional:
GET /citas?profesional_id=…&desde=…&hasta=…&estado=pendiente,confirmada, con la dirección del paciente enGET /pacientes/{id}. cita.creada,cita.actualizadaycita.canceladamantienen la ruta al día sin consultar a cada rato.- Al terminar la visita,
PATCH /citas/{id}con{"estado":"atendida"}; si la cuenta usa paquetes,sesion.firmadaconfirma la firma de asistencia.
Referencia OpenAPI
La especificación OpenAPI 3 completa (esquemas de cada recurso, parámetros y respuestas) está en /api/v1/openapi.json: impórtela en Postman, Insomnia o su generador de clientes. También puede verla en formato navegable.