VitrinaAPI

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.

Beta
This may change at any time, with a changelog entry and a notice to recent callers — see versioning.

Download the full API pública projection: openapi.json.

Book into the clinic's agenda explains this resource in prose, with runnable examples.

MethodPathWhat it does
POST/clinic/agenda/appointmentsBook a cita in the diary Vitrina owns
PATCH/clinic/agenda/appointments/{id}Move a cita
GET/clinic/agenda/availabilityFree slots, from whichever engine owns this diary
GET/clinic/agenda/diagnosticosDiagnósticos: one day of diagnosis citas with plans and abono
GET/clinic/agenda/feedAgenda for a window, any engine, by professional and branch
GET/clinic/agenda/free-hoursHoras libres: one day of free hours per professional
GET/clinic/agenda/monthAgenda month: citas per day, by Origen
GET/clinic/agenda/open-plansA patient's open treatment plans, for booking a cita into one
GET/clinic/appointments/{id}/servicesA cita's prestaciones, with what each one owes
POST/clinic/appointments/{id}/servicesAdd 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/demandWho 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

FieldTypeRequiredConstraints
slot_refstring—mín. 1
professional_iduuid—
resource_iduuid—
location_iduuid—
service_iduuid—
patient_iduuid—
contact_iduuid—
conversation_iduuid—
customer_namestring—mín. 1, máx. 200
starts_atstringyes
ends_atstringyes
notesstring—máx. 2000
allow_overbookboolean—
additionalboolean—
overbook_reasonstring—mín. 1, máx. 300
treatment_plan_iduuid—
new_treatment_planboolean—
duration_minutesinteger—≥ 5, ≤ 480
confirm_pastboolean—
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.

ParameterInTypeRequiredConstraints
idpathuuidyes

Body

FieldTypeRequiredConstraints
slot_refstring—mín. 1
professional_iduuid—
resource_iduuid—
starts_atstringyes
ends_atstringyes
allow_overbookboolean—
duration_minutesinteger—≥ 5, ≤ 480
confirm_pastboolean—
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.

ParameterInTypeRequiredConstraints
service_idqueryuuidno
professional_idqueryuuidno
location_idqueryuuidno
especialidadquerystringnomín. 1, máx. 120
fromquerystringnopatrón ^\d{4}-\d{2}-\d{2}$
toquerystringnopatrón ^\d{4}-\d{2}-\d{2}$
limitqueryintegerno≥ 1, ≤ 100
allow_overbookqueryboolean | nullno
duration_minutesquerynumberno
include_pastqueryboolean | nullno
completequeryboolean | nullno
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.

ParameterInTypeRequiredConstraints
datequerystringnopatrón ^\d{4}-\d{2}-\d{2}$
location_idqueryuuidno
professional_idqueryuuidno
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.

ParameterInTypeRequiredConstraints
fromquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
toquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
professional_idqueryuuidno
location_idqueryuuidno
resource_idqueryuuidno
statusquerypending_hold \confirmed \cancelled \
kindquerytest_drive \external \block \
originquerymeta \organic \unknown
origin_ad_idquerystringnopatrón ^[0-9]{1,32}$
diagnosisquerytrue \falseno
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.

ParameterInTypeRequiredConstraints
datequerystringyespatrón ^\d{4}-\d{2}-\d{2}$
especialidadquerystringnomín. 1, máx. 80
location_idqueryuuidno
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.

ParameterInTypeRequiredConstraints
monthquerystringyespatrón ^\d{4}-(0[1-9]|1[0-2])$
location_idqueryuuidno
professional_idqueryuuidno
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.

ParameterInTypeRequiredConstraints
patient_idqueryuuidyes
professional_idqueryuuidno
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.

ParameterInTypeRequiredConstraints
idpathuuidyes
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.

ParameterInTypeRequiredConstraints
idpathuuidyes

Body

FieldTypeRequiredConstraints
clinic_service_iduuidyes
quantityinteger—≥ 1, ≤ 99
unit_price_clpinteger | null—≥ 0, ≤ 1000000000
discount_clpinteger—≥ 0, ≤ 1000000000
tooth_or_zoneobject | 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.

ParameterInTypeRequiredConstraints
idpathuuidyes
lineIdpathuuidyes
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.

ParameterInTypeRequiredConstraints
idpathuuidyes
lineIdpathuuidyes

Body

FieldTypeRequiredConstraints
quantityinteger—≥ 1, ≤ 99
unit_price_clpinteger | null—≥ 0, ≤ 1000000000
discount_clpinteger—≥ 0, ≤ 1000000000
tooth_or_zoneobject | null—
performedboolean—
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.

ParameterInTypeRequiredConstraints
daysqueryintegerno≥ 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

On this page