Clinic agenda
A clinic's day: read the agenda, find free slots, book and move an appointment, with the same ids whichever system runs it. A connected app reads it with patient pseudonyms.
Download the full API pública projection: openapi.json.
Book into the clinic's agenda explains this resource in prose, with runnable examples.
| Method | Path | What it does |
|---|---|---|
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.
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
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 | yes | |
ends_at | string | yes | |
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"
}'Example response (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"
}
}Answers: 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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
slot_ref | string | — | mín. 1 |
professional_id | uuid | — | |
resource_id | uuid | — | |
starts_at | string | yes | |
ends_at | string | yes | |
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"
}'Example response (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"
}
}Answers: 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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
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"Example response (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
}
}Answers: 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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
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"Example response (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
}
}Answers: 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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
from | query | string | yes | patrón ^\d{4}-\d{2}-\d{2}$ |
to | query | string | yes | 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"Example response (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
}
}
]
}Answers: 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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
date | query | string | yes | 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"Example response (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
}
}Answers: 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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
month | query | string | yes | 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"Example response (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
}
]
}
}Answers: 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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
patient_id | query | uuid | yes | |
professional_id | query | uuid | no |
curl https://api.vitrinadev.com/api/v1/clinic/agenda/open-plans \
-H "Authorization: Bearer $VITRINA_KEY"Example response (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"
}Answers: 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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
curl https://api.vitrinadev.com/api/v1/clinic/appointments/<id>/services \
-H "Authorization: Bearer $VITRINA_KEY"Example response (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
}
}Answers: 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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
clinic_service_id | uuid | yes | |
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
]
}
}'Example response (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"
}
}Answers: 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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
lineId | path | uuid | yes |
curl -X DELETE https://api.vitrinadev.com/api/v1/clinic/appointments/<id>/services/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": {
"id": "9c1a0001-0000-4000-8000-000000000002",
"deleted": true
}
}Answers: 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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
lineId | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
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
}'Example response (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"
}
}Answers: 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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
days | query | integer | no | ≥ 1, ≤ 365, por defecto 30 |
curl https://api.vitrinadev.com/api/v1/clinic/waitlist/demand \
-H "Authorization: Bearer $VITRINA_KEY"Example response (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
}
]
}
]
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
Clinic diagnóstico and commercial numbers
The clinic's free Diagnóstico in one read (status, day of the window and the plan band) and its commercial numbers: presupuesto acceptance, cierres and cash, always aggregated, never a patient.
Clinic budgets
Budgets with a folio, their lines with the price frozen, and the treatment plans that order what is to be done.