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/diagnosticos | Diagnósticos: one day of diagnosis citas with plans and abono |
GET | /clinic/agenda/feed | Agenda for a window, any engine, by professional and branch |
GET | /clinic/agenda/free-hours | Horas libres: one day of free hours per professional |
GET | /clinic/agenda/month | Agenda month: citas per day, by Origen |
GET | /clinic/agenda/open-plans | A patient's open treatment plans, for booking a cita into one |
GET | /clinic/appointments/{id}/services | A cita's prestaciones, with what each one owes |
POST | /clinic/appointments/{id}/services | Add a prestación to a native cita |
DELETE | /clinic/appointments/{id}/services/{lineId} | Remove a prestación from a native cita |
PATCH | /clinic/appointments/{id}/services/{lineId} | Edit a native cita's prestación, or mark it performed |
GET | /clinic/waitlist/demand | Who is waiting for an hour, per specialty |
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. On a native clinic the cita joins a plan de tratamiento the way Dentalink does it: the patient's newest open plan with the same professional, else a new plan «Diagnóstico» (treatment_plan_id on the row); pass treatment_plan_id to book into a plan of your choice (the patient's own, not closed — else invalid_treatment_plan), or new_treatment_plan: true to open a new plan named after the visit type even when an open one exists. duration_minutes (staff) books a length other than the visit type's: the cita ends starts_at + it. A prestación with eligible professionals set refuses anyone else (professional_not_eligible). 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 |
treatment_plan_id | uuid | — | |
new_treatment_plan | boolean | — | |
duration_minutes | integer | — | ≥ 5, ≤ 480 |
confirm_past | boolean | — |
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,
"treatment_plan_id": null,
"booking_landing_id": null,
"attribution": 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,
"calendar_mirror_status": null,
"calendar_mirror_op": null,
"calendar_mirror_error": null,
"calendar_mirror_attempts": 0,
"calendar_mirror_attempted_at": null,
"calendar_mirror_next_retry_at": null,
"calendar_mirror_calendar_id": 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 | — | |
duration_minutes | integer | — | ≥ 5, ≤ 480 |
confirm_past | 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,
"treatment_plan_id": null,
"booking_landing_id": null,
"attribution": 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,
"calendar_mirror_status": null,
"calendar_mirror_op": null,
"calendar_mirror_error": null,
"calendar_mirror_attempts": 0,
"calendar_mirror_attempted_at": null,
"calendar_mirror_next_retry_at": null,
"calendar_mirror_calendar_id": 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. professional_id is the id returned by clinic_professionals_list, and each slot's professional_id is in that same id space. The window is half-open like every agenda read: from inclusive, to EXCLUSIVE. truncated: true means more free slots exist than were listed — days of the window left unread (see searched_through) or the limit cap (max 100) reached; narrow by professional_id / especialidad or shorten the window to see the rest.
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 | patrón ^\d{4}-\d{2}-\d{2}$ |
to | query | string | no | patrón ^\d{4}-\d{2}-\d{2}$ |
limit | query | integer | no | ≥ 1, ≤ 100 |
allow_overbook | query | boolean | null | no | |
duration_minutes | query | number | no | |
include_past | query | boolean | null | no | |
complete | 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/diagnosticos
Diagnósticos: one day of diagnosis citas with plans and abono
The closing routine's worklist. date (YYYY-MM-DD, the clinic's clock; default today) returns that day's diagnosis citas in time order — the same is_diagnosis rule as GET /clinic/agenda/feed — each with its attendance (attended, no_show, cancelled, booked), professional, Tipo de agendamiento and Origen, and the patient's plans of the last 12 months: from the espejo de ingresos (the vendor's plan total and abonado) for a Dentalink clinic, from its presupuestos (total and what was allocated to them) for a native one; plans of $0 are not plans. At most 3 plans per patient are listed, plans_count, plan_total and paid_total cover all of them. summary counts the day (cancelled citas are counted apart and never as Meta) and upcoming the next 5 working days (a Sunday appears only when it holds a diagnosis cita). Plans are money: without clinic_money:read every money is null and money_withheld is true; plan_created_on_day stays. Never calls the clinic API.
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 |
|---|---|---|---|---|
date | query | string | no | patrón ^\d{4}-\d{2}-\d{2}$ |
location_id | query | uuid | no | |
professional_id | query | uuid | no |
curl https://api.vitrinadev.com/api/v1/clinic/agenda/diagnosticos \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"date": "2026-09-23",
"summary": {
"total": 1,
"attended": 1,
"no_show": 0,
"cancelled": 0,
"meta": 0,
"plan_created_on_day": 1
},
"rows": [
{
"appointment_id": "eeeeeeee-0000-4000-8000-000000000001",
"display_id": "A-1",
"starts_at": "2026-09-23T13:30:00.000Z",
"ends_at": "2026-09-23T14:00:00.000Z",
"status": "completed",
"status_label": "Atendido",
"attendance": "attended",
"patient": {
"id": "12121212-0000-4000-8000-000000000001",
"name": "María José Fuentes Lagos",
"external_id": null
},
"professional": {
"id": "ffffffff-0000-4000-8000-000000000001",
"name": "Ana"
},
"service": null,
"location": {
"id": "b1b1b1b1-0000-4000-8000-000000000002",
"name": "Sucursal Maipú"
},
"booking_kind": "new",
"origin": {
"kind": "organic",
"label": "Orgánico",
"ad_id": null,
"ad_name": null,
"ad_set_name": null,
"campaign_name": null,
"first_touch_at": null,
"via": null
},
"external_id": null,
"engine": "native",
"money": {
"plans": [
{
"source": "native",
"id": "eeeeeeee-0000-4000-8000-000000000002",
"label": "E-12",
"day": "2026-09-23",
"total": 1250000,
"paid": 250000,
"status": "accepted",
"professional_name": "Ana Rojas",
"created_on_day": true
}
],
"plans_count": 1,
"plan_total": 1250000,
"paid_total": 250000
},
"plan_created_on_day": true
}
],
"upcoming": [
{
"date": "2026-09-24",
"count": 4
},
{
"date": "2026-09-25",
"count": 3
},
{
"date": "2026-09-26",
"count": 1
},
{
"date": "2026-09-28",
"count": 5
},
{
"date": "2026-09-29",
"count": 2
}
],
"money_withheld": 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. booking_kind is the cita's Tipo de agendamiento: new (Nuevo: it would be a primera visita and the patient never booked before), new_rebooked (Nuevo · reagenda: a primera visita, but the patient had earlier bookings and never came) or in_treatment (En tratamiento: every other cita); null on a row that is not a patient's cita. It uses the primera-visita rule Vitrina Ads counts, so a patient back after 12 months without an attended cita is new. origin is the patient's Origen (kind meta with the ad, ad set and campaign names, or organic) — the attribution Vitrina Ads reports, one definition; null on a row that is not a patient's cita. Filter with origin=meta|organic|unknown and, for Meta, origin_ad_id (a Meta ad id): rows that are not a patient's cita never match. is_diagnosis marks a diagnosis cita — a primera visita (booking_kind new / new_rebooked), an entry prestación the clinic confirmed, or a cita the clinic named as an evaluación / diagnóstico (the prestación, or the vendor's tratamiento label) unless its prestación is confirmed not to be an entry; diagnosis=true returns only those. booked_via says how the cita was booked: agent (Vitrina's AI agent, with the conversation channel), online (the patient on a booking page), whatsapp (a person booked it within 48 h of a WhatsApp exchange with the patient), staff (a team member in Vitrina, by), vendor (keyed straight into the clinic's system) or calendar; booked_at is the vendor's own creation time when known. patient_paid is what the patient paid on their plans, all time (espejo de ingresos abonos plus presupuesto allocations), returned only to a caller holding clinic_money:read. owed_clp is everything the cita still owes (abono AND performed prestaciones: every open receivable on the cita, net of allocations) and patient_owed_clp what its patient owes that is due by today (no deadline, or a deadline on or before today — a future cuota is not owed yet); both null without clinic_money:read. money stays the abono alone. unconfirmed is true on a live cita (status confirmed) whose clinic status says the patient has not confirmed it yet («No confirmado»): draw it as «Sin confirmar». It is a confirmation flag beside status, never a lifecycle state — the cita still counts as booked everywhere status confirmed does.
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 \ |
origin | query | meta \ | organic \ | unknown |
origin_ad_id | query | string | no | patrón ^[0-9]{1,32}$ |
diagnosis | query | true \ | false | no |
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",
"unconfirmed": false,
"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,
"booking_kind": "new",
"is_diagnosis": true,
"booked_via": {
"kind": "agent",
"channel": "whatsapp",
"by": null,
"booked_at": "2026-09-21T18:04:00.000Z"
},
"patient_paid": 250000,
"calendar_mirror_status": "synced",
"calendar_mirror_error": null,
"calendar_mirror_attempted_at": "2026-09-21T18:04:02.000Z",
"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
GET /clinic/agenda/free-hours
Horas libres: one day of free hours per professional
Every free slot of date from the clinic's own booking engine (the same read as GET /clinic/agenda/availability, whole day), grouped per professional with back-to-back slots merged into ranges and slots = how many cupos. especialidad narrows to the professionals whose specialties or note name it; especialidades lists the roster's own, for a picker. professional_id is the Vitrina uuid when the roster knows them. For a Dentalink / Medilink clinic this is one live vendor request per call (rate-limited upstream), so read it on demand rather than on a timer. A past day has no free hours.
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 |
|---|---|---|---|---|
date | query | string | sí | patrón ^\d{4}-\d{2}-\d{2}$ |
especialidad | query | string | no | mín. 1, máx. 80 |
location_id | query | uuid | no |
curl https://api.vitrinadev.com/api/v1/clinic/agenda/free-hours \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"date": "2026-09-24",
"engine": "native-clinic",
"timezone": "America/Santiago",
"especialidad": null,
"professionals": [
{
"professional_id": "ffffffff-0000-4000-8000-000000000001",
"engine_professional_id": "ffffffff-0000-4000-8000-000000000001",
"name": "Ana Rojas",
"especialidad": "Implantología",
"ranges": [
{
"starts_at": "2026-09-24T12:00:00.000Z",
"ends_at": "2026-09-24T13:30:00.000Z"
},
{
"starts_at": "2026-09-24T17:00:00.000Z",
"ends_at": "2026-09-24T18:00:00.000Z"
}
],
"slots": 5
}
],
"especialidades": [
"Endodoncia",
"Implantología",
"Ortodoncia"
],
"truncated": false
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /clinic/agenda/month
Agenda month: citas per day, by Origen
Counts per day of one month (YYYY-MM, the clinic's clock), read in one pass: citas (patients' citas that still stand), split by Origen (meta, organic, unknown — the same resolver as the feed and Vitrina Ads), diagnosis (diagnosis citas among them, the feed's is_diagnosis rule) and cancelled (anuladas, counted apart). Blocks and the workspace's own events never count. Optional location_id / professional_id (Vitrina uuids). Never calls the clinic API.
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 |
|---|---|---|---|---|
month | query | string | sí | patrón ^\d{4}-(0[1-9]|1[0-2])$ |
location_id | query | uuid | no | |
professional_id | query | uuid | no |
curl https://api.vitrinadev.com/api/v1/clinic/agenda/month \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"month": "2026-10",
"days": [
{
"date": "2026-10-01",
"citas": 42,
"meta": 9,
"organic": 20,
"unknown": 13,
"diagnosis": 7,
"cancelled": 4
},
{
"date": "2026-10-02",
"citas": 38,
"meta": 6,
"organic": 19,
"unknown": 13,
"diagnosis": 5,
"cancelled": 2
}
]
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /clinic/agenda/open-plans
A patient's open treatment plans, for booking a cita into one
The patient's plans de tratamiento that are not closed (draft, proposed, accepted, in progress), newest first, each with its professional. suggested_plan_id is the plan a booking with professional_id joins when no plan is named: the newest open plan with that professional; null means the booking opens a new one. Same shape as the Dentalink/Medilink read, so one plan step serves every engine. Requires clinic: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 |
|---|---|---|---|---|
patient_id | query | uuid | sí | |
professional_id | query | uuid | no |
curl https://api.vitrinadev.com/api/v1/clinic/agenda/open-plans \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"paciente_id": "5b0f8a54-3a43-4c1a-9a51-0e7d3c2f9a10",
"plans": [
{
"id": "9d1c2b3a-4e5f-4a6b-8c7d-1e2f3a4b5c6d",
"nombre": "Ortodoncia",
"profesional_id": "0a1b2c3d-4e5f-4a6b-9c8d-7e6f5a4b3c2d",
"profesional_nombre": "Diego Araya",
"fecha": "2026-09-14",
"total": null,
"deuda": null
}
],
"suggested_plan_id": "9d1c2b3a-4e5f-4a6b-8c7d-1e2f3a4b5c6d"
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /clinic/appointments/{id}/services
A cita's prestaciones, with what each one owes
The prestación lines of one cita (clinic_appointment_service): catalog name, quantity, the unit price and discount FROZEN on the line, total_clp (quantity × unit price − discount; 0 when unpriced), whether it was performed, the plan item it performs, and obligation_status — the line's own debt: none (not performed, or already charged through an accepted presupuesto or a pack), open (owed, possibly partly paid), paid (fully paid) or void (cancelled). editable is false with reason vendor_engine for a Dentalink / Medilink / Reservo cita (its prestaciones live in the vendor's tratamiento), cancelled for a cancelled cita, not_a_clinic_cita for any other appointment. Never calls the clinic API.
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í |
curl https://api.vitrinadev.com/api/v1/clinic/appointments/<id>/services \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"editable": true,
"reason": null,
"lines": [
{
"id": "9c1a0001-0000-4000-8000-000000000001",
"clinic_service_id": "9c1a0002-0000-4000-8000-000000000001",
"nombre": "Limpieza dental",
"quantity": 1,
"unit_price_clp": 35000,
"discount_clp": 0,
"total_clp": 35000,
"performed": true,
"performed_at": "2026-10-21T15:40:00.000Z",
"professional_id": "9c1a0003-0000-4000-8000-000000000001",
"tooth_or_zone": null,
"treatment_plan_item_id": null,
"obligation_status": "open"
},
{
"id": "9c1a0001-0000-4000-8000-000000000002",
"clinic_service_id": "9c1a0002-0000-4000-8000-000000000002",
"nombre": "Resina compuesta",
"quantity": 2,
"unit_price_clp": 45000,
"discount_clp": 10000,
"total_clp": 80000,
"performed": false,
"performed_at": null,
"professional_id": "9c1a0003-0000-4000-8000-000000000001",
"tooth_or_zone": {
"fdi": [
16,
17
]
},
"treatment_plan_item_id": "9c1a0004-0000-4000-8000-000000000001",
"obligation_status": "none"
}
],
"total_clp": 115000
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /clinic/appointments/{id}/services
Add a prestación to a native cita
Adds one line. Without unit_price_clp the price is the clinic's own resolved price for this patient on the cita's day (price list, convenio, professional price, rules — the same engine as POST /clinic/pricing/quote), frozen on the line with its explanation. A cita that belongs to a treatment plan joins the plan: the plan's first pending item of that prestación, or a new item, becomes scheduled on this cita and treatment_plan_item_id names it. A new line is not performed and owes nothing yet. A Dentalink / Medilink / Reservo cita is never edited here (its prestaciones live in the vendor's own tratamiento) and a cancelled cita is history: both answer 409 with details.reason vendor_engine / cancelled.
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 |
|---|---|---|---|
clinic_service_id | uuid | sí | |
quantity | integer | — | ≥ 1, ≤ 99 |
unit_price_clp | integer | null | — | ≥ 0, ≤ 1000000000 |
discount_clp | integer | — | ≥ 0, ≤ 1000000000 |
tooth_or_zone | object | null | — |
curl -X POST https://api.vitrinadev.com/api/v1/clinic/appointments/<id>/services \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"clinic_service_id": "9c1a0002-0000-4000-8000-000000000002",
"quantity": 2,
"discount_clp": 10000,
"tooth_or_zone": {
"fdi": [
16,
17
]
}
}'Ejemplo de respuesta (201)
{
"data": {
"id": "9c1a0001-0000-4000-8000-000000000002",
"clinic_service_id": "9c1a0002-0000-4000-8000-000000000002",
"nombre": "Resina compuesta",
"quantity": 2,
"unit_price_clp": 45000,
"discount_clp": 10000,
"total_clp": 80000,
"performed": false,
"performed_at": null,
"professional_id": "9c1a0003-0000-4000-8000-000000000001",
"tooth_or_zone": {
"fdi": [
16,
17
]
},
"treatment_plan_item_id": "9c1a0004-0000-4000-8000-000000000001",
"obligation_status": "none"
}
}Responde: 201 · 400 · 401 · 403 · 404 · 409 · 429
DELETE /clinic/appointments/{id}/services/{lineId}
Remove a prestación from a native cita
Deletes the line. Its unpaid obligation is cancelled and its plan item goes back to pending on the plan. Refused with 409 line_paid when pesos are allocated to the line's obligation. A Dentalink / Medilink / Reservo cita is never edited here (its prestaciones live in the vendor's own tratamiento) and a cancelled cita is history: both answer 409 with details.reason vendor_engine / cancelled.
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í | |
lineId | path | uuid | sí |
curl -X DELETE https://api.vitrinadev.com/api/v1/clinic/appointments/<id>/services/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"id": "9c1a0001-0000-4000-8000-000000000002",
"deleted": true
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
PATCH /clinic/appointments/{id}/services/{lineId}
Edit a native cita's prestación, or mark it performed
Changes quantity, unit price, discount, tooth / zone, or performed. PERFORMED IS DEBT: a line marked performed opens one obligation for its total in the patient's ledger (kind service, labelled with the prestación's name — which is the line a boleta for a payment against it carries), unless an accepted presupuesto item (through its plan item) or a pack session already charges for it. A price change on a performed line restates that obligation's expected amount. Performing a line linked to a plan item performs the item; un-performing puts it back to scheduled. Un-performing cancels the line's unpaid obligation; when pesos are already allocated to it the change is refused with 409 line_paid (release the payment in Pagos first). A Dentalink / Medilink / Reservo cita is never edited here (its prestaciones live in the vendor's own tratamiento) and a cancelled cita is history: both answer 409 with details.reason vendor_engine / cancelled.
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í | |
lineId | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
quantity | integer | — | ≥ 1, ≤ 99 |
unit_price_clp | integer | null | — | ≥ 0, ≤ 1000000000 |
discount_clp | integer | — | ≥ 0, ≤ 1000000000 |
tooth_or_zone | object | null | — | |
performed | boolean | — |
curl -X PATCH https://api.vitrinadev.com/api/v1/clinic/appointments/<id>/services/<id> \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"performed": true
}'Ejemplo de respuesta (200)
{
"data": {
"id": "9c1a0001-0000-4000-8000-000000000001",
"clinic_service_id": "9c1a0002-0000-4000-8000-000000000001",
"nombre": "Limpieza dental",
"quantity": 1,
"unit_price_clp": 35000,
"discount_clp": 0,
"total_clp": 35000,
"performed": true,
"performed_at": "2026-10-21T15:40:00.000Z",
"professional_id": "9c1a0003-0000-4000-8000-000000000001",
"tooth_or_zone": null,
"treatment_plan_item_id": null,
"obligation_status": "open"
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /clinic/waitlist/demand
Who is waiting for an hour, per specialty
Waitlist demand per specialty — and per professional inside it — over the last days (default 30, max 365). The specialty is the professional's (clinic_professional.especialidad), because that is what an entry pins; entries for «whoever is free» land in the specialty: null group labelled «Cualquier profesional». Every count is DISTINCT PATIENTS: waiting_now (a live entry right now — waiting or holding an offer), joined (joined inside the window) and booked (their entry became a booking inside the window). totals is counted on its own, never summed from the groups, so a patient waiting for two specialties is one person. Groups are sorted by waiting_now, then joined. Aggregates only: no patient is named. This is what the Ads capacity action reads to say «N personas esperan hora con {especialidad}».
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 |
|---|---|---|---|---|
days | query | integer | no | ≥ 1, ≤ 365, por defecto 30 |
curl https://api.vitrinadev.com/api/v1/clinic/waitlist/demand \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"window": {
"days": 30,
"from": "2026-08-31T23:00:00.000Z",
"to": "2026-09-30T23:00:00.000Z"
},
"totals": {
"waiting_now": 9,
"joined": 11,
"booked": 2
},
"by_specialty": [
{
"specialty": "TTM",
"label": "TTM",
"waiting_now": 7,
"joined": 8,
"booked": 1,
"professionals": [
{
"professional_id": "d4d4d4d4-0000-4000-8000-000000000001",
"name": "Dra. Profesional Ejemplo",
"waiting_now": 7,
"joined": 8,
"booked": 1
}
]
},
{
"specialty": null,
"label": "Cualquier profesional",
"waiting_now": 2,
"joined": 3,
"booked": 1,
"professionals": [
{
"professional_id": null,
"name": null,
"waiting_now": 2,
"joined": 3,
"booked": 1
}
]
}
]
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
Diagnóstico y números comerciales de la clínica
El Diagnóstico gratuito de la clínica en una lectura (estado, día de la ventana y la banda de plan) y sus números comerciales: aceptación de presupuestos, cierres y caja, siempre agregados, nunca un paciente.
Presupuestos de la clínica
Presupuestos con folio, sus líneas con el precio congelado y los planes de tratamiento que ordenan lo que se va a hacer.