Reservar una hora en la clínica
Leer la agenda por API, encontrar horas libres y confirmar la cita.
ClínicasSolo en los workspaces de clínicas.
GET /clinic/agenda/feed devuelve las citas de la clínica entre dos fechas. Con esos mismos ids se buscan horas libres, se reserva y se mueve una cita. Por debajo la agenda puede vivir en Medilink, en Reservo o en Vitrina, y el contrato hacia afuera no cambia.
curl "https://api.vitrinadev.com/api/v1/clinic/agenda/feed?from=2026-09-23&to=2026-09-24" \
-H "Authorization: Bearer $VITRINA_KEY"La agenda no es la ficha clínica. La evolución, las notas firmadas, los antecedentes, los consentimientos y los documentos son datos de salud. Están marcados como sensibles en el contrato y no se publican en este capítulo. De aquí sale el calendario; más abajo está dónde pasa la línea.
Leer la agenda
GET /clinic/agenda/feed devuelve las citas entre from (incluido) y to (excluido), dos días YYYY-MM-DD en la zona horaria de la clínica. La ventana no puede pasar de ocho días. Las citas anuladas vienen, con su status: una agenda que las esconde no deja saber por qué quedó un hueco.
Se filtra con ids de Vitrina: professional_id, location_id, resource_id (el box o sillón), status y kind. Los dos últimos aceptan listas separadas por coma. Así viene una cita, tal como la devolvió a una API key de la clínica:
{
"data": [
{
"id": "eeeeeeee-0000-4000-8000-000000000001",
"display_id": "A-1",
"engine": "native",
"external_id": null,
"kind": "clinic",
"status": "confirmed",
"status_label": null,
"starts_at": "2026-09-23T13:30:00.000Z",
"ends_at": "2026-09-23T14:00:00.000Z",
"arrival_state": "scheduled",
"professional": { "id": "ffffffff-0000-4000-8000-000000000001", "name": "Ana" },
"location": { "id": "b1b1b1b1-0000-4000-8000-000000000002", "name": "Sucursal Maipú" },
"resource": null,
"service": null,
"patient": {
"id": "12121212-0000-4000-8000-000000000001",
"name": "María José Fuentes Lagos",
"external_id": null
},
"contact": { "id": "22222222-0000-4000-8000-000000000001", "name": "María José Fuentes Lagos" },
"customer_name": "María José Fuentes Lagos",
"notes": "Avisar al +56987654321 si se atrasa",
"money": null,
"flags": [
{ "id": "13131313-0000-4000-8000-000000000001", "kind": "alergia", "label": "Alergia a penicilina", "severity": "severa" }
],
"vendor": { "professional_id": null, "professional_name": null, "sucursal_id": null, "agenda_id": null, "sillon": null, "status_code": null }
}
]
}Lo que conviene saber de cada campo:
enginedice qué sistema lleva esa cita.nativees la agenda de Vitrina;healthatomoreservo, una cita que vive en el sistema de la clínica y que Vitrina refleja. Una cita reflejada se lee igual, pero se mueve en su sistema.statuses la categoría:confirmed,cancelled,completed,no_show.status_labeles la palabra de la clínica, «Confirmado» o «Llegó», que es la que reconoce quien trabaja la agenda.arrival_statees dónde está el paciente ahora:scheduled,waiting,in_room,to_pay,done.patientes la ficha de la clínica ycontactla persona con la que la clínica conversa. Suelen ser la misma, y no siempre: un apoderado agenda por un menor.flagsson las alertas que la clínica decidió mostrar sobre la cita: alergias, riesgo, «no agendar sin abono». Son contenido de la ficha, y por eso una aplicación conectada no las recibe nunca (más abajo).vendorrepite cómo llamó el sistema de la clínica a lo mismo, como procedencia. Una cita convendor.professional_idyprofessional: nullmarca un profesional que falta en el listado de Vitrina; la cita sí tiene quien la atienda. En una cita de Vitrina todovendoresnull.
Horas libres
GET /clinic/agenda/availability responde con las horas libres de quien lleva la agenda. En Medilink es su respuesta en vivo. En Reservo, el cálculo local sobre lo que Vitrina refleja. En la agenda de Vitrina, el horario de cada profesional menos sus excepciones, sus citas y los boxes ocupados. Filtra por professional_id, location_id, service_id o especialidad, desde from:
{
"data": {
"engine": "native-clinic",
"timezone": "America/Santiago",
"slots": [
{
"starts_at": "2026-09-24T12:00:00.000Z",
"ends_at": "2026-09-24T12:30:00.000Z",
"label": "jueves, 24 de septiembre, 09:00",
"slot_ref": "ncl1_eyJwIjoiZmZmZmZmZmYtMDAwMC00MDAw…",
"professional_id": "ffffffff-0000-4000-8000-000000000001",
"professional_name": "Ana Rojas"
}
],
"searched_through": "2026-09-24",
"truncated": false
}
}slot_ref es opaco. Fija el profesional, el box y la sede de esa hora. No lo armes ni lo interpretes; devuélvelo tal cual al reservar.
allow_overbook viene apagado: las horas de sobrecupo no se ofrecen por defecto.
Reservar y mover
Las dos escrituras piden clinic:write.
POST /clinic/agenda/appointments reserva una cita. Pasa el slot_ref de una hora libre, o un professional_id con starts_at y ends_at:
curl -X POST https://api.vitrinadev.com/api/v1/clinic/agenda/appointments \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: reserva-2026-09-24-mfuentes" \
-d '{
"professional_id": "ffffffff-0000-4000-8000-000000000001",
"location_id": "b1b1b1b1-0000-4000-8000-000000000002",
"patient_id": "12121212-0000-4000-8000-000000000001",
"starts_at": "2026-09-24T14:00:00.000Z",
"ends_at": "2026-09-24T14:30:00.000Z",
"notes": "Control mensual"
}'La reserva pasa por el sistema que lleva la agenda. En una clínica con Medilink la decide Medilink, y si la rechaza recibes su respuesta. En la agenda de Vitrina quedan la cita y su prestación, con la duración y el precio ya congelados. Si hay política de abono, queda también lo que el paciente debe pagar. Cuando la hora ya está tomada, la respuesta es slot_taken con alternativas.
Manda siempre una Idempotency-Key. Si la red corta la respuesta y reintentas con la misma, recibes la misma cita en vez de dos (Errores).
PATCH /clinic/agenda/appointments/{id} mueve una cita con starts_at y ends_at nuevos, y opcionalmente otro professional_id o resource_id. El plazo del abono se mueve con ella.
Cada reserva, cada cambio de hora y cada anulación deja además un evento: appointment.booked, appointment.rescheduled y appointment.cancelled.
Lo que ve una aplicación conectada
Una aplicación conectada (Claude, ChatGPT o cualquier cliente que la clínica autorizó por OAuth) lee la misma agenda, con tres diferencias. Esto recibió una, sobre la misma cita, en una clínica que no permitió nombres de pacientes:
{
"data": [
{
"id": "eeeeeeee-0000-4000-8000-000000000001",
"starts_at": "2026-09-23T13:30:00.000Z",
"professional": { "id": "ffffffff-0000-4000-8000-000000000001", "name": "Ana" },
"location": { "id": "b1b1b1b1-0000-4000-8000-000000000002", "name": "Sucursal Maipú" },
"patient": { "id": "12121212-0000-4000-8000-000000000001", "name": "M.F. · #1001", "external_id": null },
"contact": { "id": "22222222-0000-4000-8000-000000000001", "name": "M.F. · #1001" },
"customer_name": "M.F. · s/n",
"notes": "Avisar al [teléfono oculto] si se atrasa"
}
],
"meta": {
"patient_privacy": {
"mode": "pseudonymized",
"subjects": 3,
"notice": "Patients in this result appear as pseudonyms: initials plus a stable number…"
}
}
}- Cada paciente es un seudónimo: sus iniciales y un número estable,
M.F. · #1001. El mismo número es la misma persona en cada llamada y en cada herramienta. Así se puede contar, agrupar y seguir a un paciente sin saber quién es. El RUT, el teléfono y el correo se tapan también dentro del texto libre.meta.patient_privacyavisa que la respuesta viene así. - Las alertas clínicas no llegan. No hay
flags, ni vacío ni con nada. Son contenido de la ficha, y leer esas mismas alertas por su cuenta es una operación sensible. - La ficha no se alcanza. El registro de pacientes, la evolución, los consentimientos y los documentos responden
403 CONNECTED_APP_SENSITIVE_DATAa una aplicación conectada. El código es propio para que tu integración no lo confunda con un scope que falta. Pedir más scopes no lo arregla; lo arregla usar otra clase de credencial.
{
"error": {
"code": "CONNECTED_APP_SENSITIVE_DATA",
"message": "Esta operación entrega datos personales sensibles (ficha clínica, registro de pacientes, consentimientos o documentos clínicos). Una aplicación conectada no puede leerlos…"
}
}Una clínica puede permitir nombres desde Configuración › Conexiones › MCP, y la decisión es del dueño o de un administrador. Con eso la aplicación conectada recibe los nombres tal como una API key. Cada lectura que muestra a un paciente queda en el registro de accesos de la ficha. La línea nombra a la persona detrás de la credencial y a la aplicación: «[email protected] vía Claude». Lo que se permitió son nombres: las alertas clínicas siguen sin llegar y la ficha sigue cerrada.
Lo que no es JSON no se puede entregar con seudónimos. A una aplicación conectada de una clínica se le responden con 403 CONNECTED_APP_SENSITIVE_DATA la exportación de contactos en CSV, una conversación en markdown y un flujo. Vale con nombres permitidos o sin ellos.
Una API key del workspace o un token personal no pasan por nada de esto. Actúan dentro de la clínica y reciben la cita completa, como en el primer ejemplo. Por qué la frontera está ahí lo explica Datos personales y de salud.
Lo que no está publicado
- La ficha clínica, el registro de pacientes, los consentimientos y los documentos tienen sus propios capítulos: pacientes, ficha, consentimientos y documentos. Están marcados
x-vitrina-sensitive: piden su propio permiso, registran cada lectura y no los alcanza ninguna aplicación conectada. - El mapa de sedes del sistema de la clínica (
/clinic/sucursales). Es el selector del panel de Conexión: habla con los ids de ese sistema y configura el reflejo.
La suscripción a los eventos de la agenda está armada de punta a punta en Llevar las citas a tu sistema.