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/feedAgenda for a window, any engine, by professional and branch

POST /clinic/agenda/appointments

Book a cita in the diary Vitrina owns

Routes through the tenant's booking engine, so a vendor clinic gets its own engine's honest refusal rather than a local row the vendor never hears about. Pass either the slot_ref from GET /clinic/agenda/availability or a professional_id outright (a drag-to-create on the grid has no ref). One transaction: the cita, its prestación line with the duration and price FROZEN, and — when a deposit policy applies — the customer_obligation the ledger settles against. A collision is refused with slot_taken and fresh alternatives unless allow_overbook is set, which also requires a reason: a sobrecupo nobody can explain is indistinguishable from a double booking. Requires clinic:write, which no connected app holds by default.

Connected apps: every patient and contact in the response is a Seudónimo de paciente — initials plus a stable number, "M.F. · #1001" — unless the clinic allowed patient names, with RUT, phone and email masked in free text and meta.patient_privacy saying so. Clinical alerts (flags) are withheld in both modes, and a non-JSON body (an export, a file) is refused with 403 CONNECTED_APP_SENSITIVE_DATA.

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
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,
    "booking_landing_id": null,
    "starts_at": "2026-09-24T14:00:00.000Z",
    "ends_at": "2026-09-24T14:30:00.000Z",
    "hold_expires_at": null,
    "google_event_id": null,
    "google_ical_uid": null,
    "google_updated_at": null,
    "google_etag": null,
    "reminder_at": null,
    "reminder_sent_at": null,
    "reminder_job_id": null,
    "customer_name": "María José Fuentes Lagos",
    "notes": "Control mensual",
    "metadata": {
      "engine": "native-clinic",
      "patient_rut": "11.111.111-1",
      "clinic_patient_id": "12121212-0000-4000-8000-000000000001"
    },
    "display_seq": 2,
    "display_id": "A-2",
    "created_at": "2026-09-22T17:18:58.202Z",
    "updated_at": "2026-09-22T17:18:58.202Z"
  }
}

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—
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,
    "booking_landing_id": null,
    "starts_at": "2026-09-24T14:30:00.000Z",
    "ends_at": "2026-09-24T15:00:00.000Z",
    "hold_expires_at": null,
    "google_event_id": null,
    "google_ical_uid": null,
    "google_updated_at": null,
    "google_etag": null,
    "reminder_at": null,
    "reminder_sent_at": null,
    "reminder_job_id": null,
    "customer_name": "María José Fuentes Lagos",
    "notes": "Control mensual",
    "metadata": {
      "engine": "native-clinic",
      "patient_rut": "11.111.111-1",
      "clinic_patient_id": "12121212-0000-4000-8000-000000000001"
    },
    "display_seq": 2,
    "display_id": "A-2",
    "created_at": "2026-09-22T17:18:58.202Z",
    "updated_at": "2026-09-22T17:19:15.981Z"
  }
}

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.

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
fromquerystringno
toquerystringno
limitqueryintegerno≥ 1, ≤ 100
allow_overbookqueryboolean | 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/feed

Agenda for a window, any engine, by professional and branch

Mirrored AND native appointments between from (inclusive) and to (EXCLUSIVE), bare YYYY-MM-DD days in the clinic's timezone; the window may not exceed 8 days. Cancelled citas are INCLUDED (the grid draws them muted). Unlike GET /clinic/agenda, professional_id and location_id are VITRINA uuids — clinic_professional.id and location.id — so one query serves a Medilink, a Reservo and a native clinic. Each row still carries the vendor's own ids under vendor for provenance: a row with vendor.professional_id set and professional: null is a roster gap, never a cita nobody is attending. Never calls the clinic API. flags are the clinical alerts the clinic shows on the block: clinical-record content, returned to an API key or personal token only when it also holds clinic_record:read.

Connected apps: every patient and contact in the response is a Seudónimo de paciente — initials plus a stable number, "M.F. · #1001" — unless the clinic allowed patient names, with RUT, phone and email masked in free text and meta.patient_privacy saying so. Clinical alerts (flags) are withheld in both modes, and a non-JSON body (an export, a file) is refused with 403 CONNECTED_APP_SENSITIVE_DATA.

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 \
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",
      "clinic_status_id": null,
      "status_label": null,
      "status_color": null,
      "starts_at": "2026-09-23T13:30:00.000Z",
      "ends_at": "2026-09-23T14:00:00.000Z",
      "arrival_state": "scheduled",
      "professional": {
        "id": "ffffffff-0000-4000-8000-000000000001",
        "name": "Ana"
      },
      "location": {
        "id": "b1b1b1b1-0000-4000-8000-000000000002",
        "name": "Sucursal Maipú"
      },
      "resource": null,
      "service": null,
      "patient": {
        "id": "12121212-0000-4000-8000-000000000001",
        "name": "María José Fuentes Lagos",
        "external_id": null
      },
      "contact": {
        "id": "22222222-0000-4000-8000-000000000001",
        "name": "María José Fuentes Lagos"
      },
      "customer_name": "María José Fuentes Lagos",
      "notes": "Avisar al +56987654321 si se atrasa",
      "money": null,
      "flags": [
        {
          "id": "13131313-0000-4000-8000-000000000001",
          "kind": "alergia",
          "label": "Alergia a penicilina",
          "severity": "severa"
        }
      ],
      "vendor": {
        "professional_id": null,
        "professional_name": null,
        "sucursal_id": null,
        "agenda_id": null,
        "sillon": null,
        "status_code": null
      }
    }
  ]
}

Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429

On this page