Agenda de la clínica
El día de una clínica: leer la agenda, buscar horas libres, reservar y mover una cita, con los mismos ids sea cual sea el sistema que la lleva. Una aplicación conectada la lee con seudónimos de paciente.
Descarga la proyección completa de la API pública: openapi.json.
Reservar una hora en la clínica explica este recurso en prosa, con ejemplos ejecutables.
| Método | Ruta | Qué hace |
|---|---|---|
POST | /clinic/agenda/appointments | Book a cita in the diary Vitrina owns |
PATCH | /clinic/agenda/appointments/{id} | Move a cita |
GET | /clinic/agenda/availability | Free slots, from whichever engine owns this diary |
GET | /clinic/agenda/feed | Agenda for a window, any engine, by professional and branch |
POST /clinic/agenda/appointments
Book a cita in the diary Vitrina owns
Routes through the tenant's booking engine, so a vendor clinic gets its own engine's honest refusal rather than a local row the vendor never hears about. Pass either the slot_ref from GET /clinic/agenda/availability or a professional_id outright (a drag-to-create on the grid has no ref). One transaction: the cita, its prestación line with the duration and price FROZEN, and — when a deposit policy applies — the customer_obligation the ledger settles against. A collision is refused with slot_taken and fresh alternatives unless allow_overbook is set, which also requires a reason: a sobrecupo nobody can explain is indistinguishable from a double booking. Requires clinic:write, which no connected app holds by default.
Connected apps: every patient and contact in the response is a Seudónimo de paciente — initials plus a stable number, "M.F. · #1001" — unless the clinic allowed patient names, with RUT, phone and email masked in free text and meta.patient_privacy saying so. Clinical alerts (flags) are withheld in both modes, and a non-JSON body (an export, a file) is refused with 403 CONNECTED_APP_SENSITIVE_DATA.
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
slot_ref | string | — | mín. 1 |
professional_id | uuid | — | |
resource_id | uuid | — | |
location_id | uuid | — | |
service_id | uuid | — | |
patient_id | uuid | — | |
contact_id | uuid | — | |
conversation_id | uuid | — | |
customer_name | string | — | mín. 1, máx. 200 |
starts_at | string | sí | |
ends_at | string | sí | |
notes | string | — | máx. 2000 |
allow_overbook | boolean | — | |
additional | boolean | — | |
overbook_reason | string | — | mín. 1, máx. 300 |
curl -X POST https://api.vitrinadev.com/api/v1/clinic/agenda/appointments \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-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"
}'Ejemplo de respuesta (201)
{
"data": {
"id": "eeeeeeee-0000-4000-8000-000000000002",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"lead_id": null,
"contact_id": null,
"conversation_id": null,
"vehicle_id": null,
"appointment_type_id": null,
"owner_user_id": null,
"location_id": "b1b1b1b1-0000-4000-8000-000000000002",
"professional_id": "ffffffff-0000-4000-8000-000000000001",
"resource_id": null,
"kind": "clinic",
"source": "agent",
"engine": "native",
"external_id": null,
"status": "confirmed",
"status_epoch": 0,
"clinic_status_id": null,
"arrival_state": "scheduled",
"arrived_at": null,
"in_room_at": null,
"done_at": null,
"series_id": null,
"series_index": null,
"booking_landing_id": null,
"starts_at": "2026-09-24T14:00:00.000Z",
"ends_at": "2026-09-24T14:30:00.000Z",
"hold_expires_at": null,
"google_event_id": null,
"google_ical_uid": null,
"google_updated_at": null,
"google_etag": null,
"reminder_at": null,
"reminder_sent_at": null,
"reminder_job_id": null,
"customer_name": "María José Fuentes Lagos",
"notes": "Control mensual",
"metadata": {
"engine": "native-clinic",
"patient_rut": "11.111.111-1",
"clinic_patient_id": "12121212-0000-4000-8000-000000000001"
},
"display_seq": 2,
"display_id": "A-2",
"created_at": "2026-09-22T17:18:58.202Z",
"updated_at": "2026-09-22T17:18:58.202Z"
}
}Responde: 201 · 400 · 401 · 403 · 404 · 409 · 429
PATCH /clinic/agenda/appointments/{id}
Move a cita
Reschedule through the owning engine — a native cita moves inside one transaction under the same per-professional lock a booking takes, and a mirrored one moves the way its vendor moves citas. The deposit's deadline travels with it: a cita moved a week out with a deadline that stayed put is a "vencido" nobody can date. professional_id / resource_id move the cita to another agenda or room in the same stroke; omitted, it keeps the ones it has. Requires clinic:write.
Connected apps: every patient and contact in the response is a Seudónimo de paciente — initials plus a stable number, "M.F. · #1001" — unless the clinic allowed patient names, with RUT, phone and email masked in free text and meta.patient_privacy saying so. Clinical alerts (flags) are withheld in both modes, and a non-JSON body (an export, a file) is refused with 403 CONNECTED_APP_SENSITIVE_DATA.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
slot_ref | string | — | mín. 1 |
professional_id | uuid | — | |
resource_id | uuid | — | |
starts_at | string | sí | |
ends_at | string | sí | |
allow_overbook | boolean | — |
curl -X PATCH https://api.vitrinadev.com/api/v1/clinic/agenda/appointments/<id> \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"starts_at": "2026-09-24T14:30:00.000Z",
"ends_at": "2026-09-24T15:00:00.000Z"
}'Ejemplo de respuesta (200)
{
"data": {
"id": "eeeeeeee-0000-4000-8000-000000000002",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"lead_id": null,
"contact_id": null,
"conversation_id": null,
"vehicle_id": null,
"appointment_type_id": null,
"owner_user_id": null,
"location_id": "b1b1b1b1-0000-4000-8000-000000000002",
"professional_id": "ffffffff-0000-4000-8000-000000000001",
"resource_id": null,
"kind": "clinic",
"source": "agent",
"engine": "native",
"external_id": null,
"status": "confirmed",
"status_epoch": 0,
"clinic_status_id": null,
"arrival_state": "scheduled",
"arrived_at": null,
"in_room_at": null,
"done_at": null,
"series_id": null,
"series_index": null,
"booking_landing_id": null,
"starts_at": "2026-09-24T14:30:00.000Z",
"ends_at": "2026-09-24T15:00:00.000Z",
"hold_expires_at": null,
"google_event_id": null,
"google_ical_uid": null,
"google_updated_at": null,
"google_etag": null,
"reminder_at": null,
"reminder_sent_at": null,
"reminder_job_id": null,
"customer_name": "María José Fuentes Lagos",
"notes": "Control mensual",
"metadata": {
"engine": "native-clinic",
"patient_rut": "11.111.111-1",
"clinic_patient_id": "12121212-0000-4000-8000-000000000001"
},
"display_seq": 2,
"display_id": "A-2",
"created_at": "2026-09-22T17:18:58.202Z",
"updated_at": "2026-09-22T17:19:15.981Z"
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /clinic/agenda/availability
Free slots, from whichever engine owns this diary
One endpoint, three truths: a Medilink clinic gets its vendor's live answer, a Reservo clinic the local compute over its mirror, and a native clinic the professionals' work patterns minus exceptions, existing citas and room contention. Each slot carries an opaque slot_ref that pins the professional, the box and the branch — book it back verbatim. allow_overbook is a STAFF flag and defaults to false: an hour that only exists because the clinic permits sobrecupo is a receptionist's decision with a reason, never something a patient-facing read gives away.
Connected apps: every patient and contact in the response is a Seudónimo de paciente — initials plus a stable number, "M.F. · #1001" — unless the clinic allowed patient names, with RUT, phone and email masked in free text and meta.patient_privacy saying so. Clinical alerts (flags) are withheld in both modes, and a non-JSON body (an export, a file) is refused with 403 CONNECTED_APP_SENSITIVE_DATA.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
service_id | query | uuid | no | |
professional_id | query | uuid | no | |
location_id | query | uuid | no | |
especialidad | query | string | no | mín. 1, máx. 120 |
from | query | string | no | |
to | query | string | no | |
limit | query | integer | no | ≥ 1, ≤ 100 |
allow_overbook | query | boolean | null | no |
curl https://api.vitrinadev.com/api/v1/clinic/agenda/availability \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"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_eyJwIjoiZmZmZmZmZmYtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwibCI6ImIxYjFiMWIxLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMiIsInMiOiIyMDI2LTA5LTI0VDEyOjAwOjAwLjAwMFoiLCJlIjoiMjAyNi0wOS0yNFQxMjozMDowMC4wMDBaIn0",
"professional_id": "ffffffff-0000-4000-8000-000000000001",
"professional_name": "Ana Rojas"
},
{
"starts_at": "2026-09-24T12:30:00.000Z",
"ends_at": "2026-09-24T13:00:00.000Z",
"label": "jueves, 24 de septiembre, 09:30",
"slot_ref": "ncl1_eyJwIjoiZmZmZmZmZmYtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwibCI6ImIxYjFiMWIxLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMiIsInMiOiIyMDI2LTA5LTI0VDEyOjMwOjAwLjAwMFoiLCJlIjoiMjAyNi0wOS0yNFQxMzowMDowMC4wMDBaIn0",
"professional_id": "ffffffff-0000-4000-8000-000000000001",
"professional_name": "Ana Rojas"
}
],
"searched_through": "2026-09-24",
"truncated": false
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /clinic/agenda/feed
Agenda for a window, any engine, by professional and branch
Mirrored AND native appointments between from (inclusive) and to (EXCLUSIVE), bare YYYY-MM-DD days in the clinic's timezone; the window may not exceed 8 days. Cancelled citas are INCLUDED (the grid draws them muted). Unlike GET /clinic/agenda, professional_id and location_id are VITRINA uuids — clinic_professional.id and location.id — so one query serves a Medilink, a Reservo and a native clinic. Each row still carries the vendor's own ids under vendor for provenance: a row with vendor.professional_id set and professional: null is a roster gap, never a cita nobody is attending. Never calls the clinic API. flags are the clinical alerts the clinic shows on the block: clinical-record content, returned to an API key or personal token only when it also holds clinic_record:read.
Connected apps: every patient and contact in the response is a Seudónimo de paciente — initials plus a stable number, "M.F. · #1001" — unless the clinic allowed patient names, with RUT, phone and email masked in free text and meta.patient_privacy saying so. Clinical alerts (flags) are withheld in both modes, and a non-JSON body (an export, a file) is refused with 403 CONNECTED_APP_SENSITIVE_DATA.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
from | query | string | sí | patrón ^\d{4}-\d{2}-\d{2}$ |
to | query | string | sí | patrón ^\d{4}-\d{2}-\d{2}$ |
professional_id | query | uuid | no | |
location_id | query | uuid | no | |
resource_id | query | uuid | no | |
status | query | pending_hold \ | confirmed \ | cancelled \ |
kind | query | test_drive \ | external \ | block \ |
curl https://api.vitrinadev.com/api/v1/clinic/agenda/feed \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": [
{
"id": "eeeeeeee-0000-4000-8000-000000000001",
"display_id": "A-1",
"engine": "native",
"external_id": null,
"kind": "clinic",
"status": "confirmed",
"clinic_status_id": null,
"status_label": null,
"status_color": 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
}
}
]
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429