VitrinaAPI

Open the clinic's calendar to your software

Read and write the calendar from your own system, without reaching the record.

ClinicsOnly in clinic workspaces.

The clinic's practice-management software already exists, and it already has its own calendar. This recipe connects it to Vitrina's, so it can read and book hours without anybody retyping anything. By the end your system books, moves and reads appointments directly, and never sees a clinical record.

Trap

An sk_ key receives the patient's name

A key with clinic:read receives the name of whoever holds the slot. The pseudonym, the «J.P. · #4821» kind, only applies to an app connected over OAuth. The record (history, clinical notes, agenda flags) doesn't arrive without clinic_record:read.

Before you start

  • A clinic-type tenant, with professionals and services already loaded (Clinical catalogue has the order).
  • A key with clinic:read and clinic:write, and nothing else. No clinic_patients:read, no clinic_record:read: the calendar doesn't ask for them, and adding them widens what a leaked key exposes.
  • If the clinic runs on Dentalink, Medilink or Reservo, the catalogue is edited there. Vitrina only mirrors it.

1. Resolve who and what each slot belongs to

The calendar only carries ids. Before you can show anything readable, your system needs the catalogue behind them:

curl "https://api.vitrinadev.com/api/v1/clinic/professionals?limit=1" \
  -H "Authorization: Bearer $VITRINA_KEY"
{
  "data": {
    "data": [
      {
        "id": "63f237cc-8503-42b4-ad56-b884431be966",
        "source": "native",
        "nombre": "Benjamín",
        "apellidos": "Pinilla Irarrázabal",
        "especialidad": "Ortodoncia",
        "agenda_online": true,
        "intervalo_minutes": 15,
        "active": true
      }
    ],
    "total": 5,
    "page": 1,
    "limit": 1
  }
}

source: "native" matters as much as the name. It marks this professional as managed inside Vitrina. If it named a vendor instead, editing it this way would answer 409, covered below.

2. Read the calendar for a window

curl "https://api.vitrinadev.com/api/v1/clinic/agenda/feed?from=2026-09-21&to=2026-09-23" \
  -H "Authorization: Bearer $VITRINA_KEY"
{
  "data": [
    {
      "id": "4c5a9b18-e565-45c8-8472-93e81454d5e7",
      "display_id": "A-4270",
      "engine": "native",
      "kind": "clinic",
      "status": "completed",
      "starts_at": "2026-09-21T12:00:00.000Z",
      "ends_at": "2026-09-21T12:15:00.000Z",
      "professional": { "id": "e0782bc5-c483-4d9d-99b2-76515de2a2f0", "name": "Magdalena" },
      "location": { "id": "5d091787-f540-4869-b4d0-50f3b5402b74", "name": "Aurora Providencia" },
      "service": { "id": "546db704-ea02-4066-9d6e-9f0f2c1dc809", "name": "Radiografía periapical" },
      "patient": { "id": "62bcf8c9-f2d4-4191-99a3-bdab357e5952", "name": "Constanza Alcaíno Yáñez", "external_id": null },
      "notes": null
    }
  ]
}

The patient's name travels in full. flags doesn't show up in the response: it only reaches a key with clinic_record:read, carrying the block's clinical alerts.

3. Find an open slot and book it

curl "https://api.vitrinadev.com/api/v1/clinic/agenda/availability?service_id=0c1e0c10-6d1c-4ff9-a4a6-e1109a46e507&from=2026-09-28&to=2026-09-30&limit=1" \
  -H "Authorization: Bearer $VITRINA_KEY"
{
  "data": {
    "engine": "native-clinic",
    "timezone": "America/Santiago",
    "slots": [
      {
        "starts_at": "2026-09-28T13:00:00.000Z",
        "ends_at": "2026-09-28T13:40:00.000Z",
        "label": "lunes, 28 de septiembre, 10:00",
        "slot_ref": "ncl1_eyJwIjoiNjNmMjM3Y2MtODUwMy00MmI0LWFkNTYtYjg4NDQzMWJlOTY2IiwiciI6IjkxMDAwNjJmLTFjYmYtNGIyOC05M2M5LWU0Y2E5NDg0NDNjMSIsImwiOiI1ZDA5MTc4Ny1mNTQwLTQ4NjktYjRkMC01MGYzYjU0MDJiNzQiLCJzIjoiMjAyNi0wOS0yOFQxMzowMDowMC4wMDBaIiwiZSI6IjIwMjYtMDktMjhUMTM6NDA6MDAuMDAwWiJ9",
        "professional_id": "63f237cc-8503-42b4-ad56-b884431be966",
        "professional_name": "Benjamín Pinilla Irarrázabal"
      }
    ],
    "searched_through": "2026-09-28",
    "truncated": true
  }
}

slot_ref encodes the professional, the room and the time in one opaque token. You don't build it by hand: you pass it straight back, along with starts_at and ends_at, which the body still asks for even though they repeat inside the token.

curl -X POST https://api.vitrinadev.com/api/v1/clinic/agenda/appointments \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "slot_ref": "ncl1_eyJwIjoiNjNmMjM3Y2MtODUwMy00MmI0LWFkNTYtYjg4NDQzMWJlOTY2IiwiciI6IjkxMDAwNjJmLTFjYmYtNGIyOC05M2M5LWU0Y2E5NDg0NDNjMSIsImwiOiI1ZDA5MTc4Ny1mNTQwLTQ4NjktYjRkMC01MGYzYjU0MDJiNzQiLCJzIjoiMjAyNi0wOS0yOFQxMzowMDowMC4wMDBaIiwiZSI6IjIwMjYtMDktMjhUMTM6NDA6MDAuMDAwWiJ9",
    "service_id": "0c1e0c10-6d1c-4ff9-a4a6-e1109a46e507",
    "customer_name": "Prueba PMS externo",
    "starts_at": "2026-09-28T13:00:00.000Z",
    "ends_at": "2026-09-28T13:40:00.000Z"
  }'
{
  "data": {
    "id": "bacf8d6f-2076-4dcc-8339-771bcffe8872",
    "display_id": "A-4301",
    "professional_id": "63f237cc-8503-42b4-ad56-b884431be966",
    "resource_id": "9100062f-1cbf-4b28-93c9-e4ca948443c1",
    "location_id": "5d091787-f540-4869-b4d0-50f3b5402b74",
    "kind": "clinic",
    "engine": "native",
    "status": "confirmed",
    "starts_at": "2026-09-28T13:00:00.000Z",
    "ends_at": "2026-09-28T13:40:00.000Z",
    "customer_name": "Prueba PMS externo",
    "notes": null,
    "metadata": { "engine": "native-clinic", "treatment_name": "Exodoncia simple", "clinic_service_id": "0c1e0c10-6d1c-4ff9-a4a6-e1109a46e507" }
  }
}

A-4301, confirmed right away: there's no separate holding step. resource_id came back on its own, resolved from the same slot_ref, so nothing needs to ask the user for the room.

4. Move it

curl -X PATCH https://api.vitrinadev.com/api/v1/clinic/agenda/appointments/bacf8d6f-2076-4dcc-8339-771bcffe8872 \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "professional_id": "63f237cc-8503-42b4-ad56-b884431be966",
    "resource_id": "9100062f-1cbf-4b28-93c9-e4ca948443c1",
    "starts_at": "2026-09-28T14:00:00.000Z",
    "ends_at": "2026-09-28T14:40:00.000Z"
  }'
{
  "data": {
    "id": "bacf8d6f-2076-4dcc-8339-771bcffe8872",
    "display_id": "A-4301",
    "status": "confirmed",
    "starts_at": "2026-09-28T14:00:00.000Z",
    "ends_at": "2026-09-28T14:40:00.000Z",
    "updated_at": "2026-09-23T11:58:10.586Z"
  }
}

Same id, same display_id: it's the same appointment, moved. professional_id and resource_id go in again in full, for when your system also wants to change who's attending along with the time.

When it fails

This same key, with only clinic:read and clinic:write, can't read the patient register:

curl "https://api.vitrinadev.com/api/v1/clinic/patients?limit=2" \
  -H "Authorization: Bearer $VITRINA_KEY"
{
  "error": {
    "code": "FORBIDDEN",
    "message": "Missing required scope: clinic_patients:read",
    "requestId": "5ed678e2-d31c-4a6b-b976-81e9d3f91757"
  }
}

If the clinic runs on Dentalink, Medilink or Reservo, there's a separate lock. Creating or editing a professional, a service or a patient this way answers 409, with the reason not_native_source: that resource is managed at the vendor. The calendar doesn't carry that lock. Booking and moving appointments keeps working the same, because an appointment writes through to the vendor, not to its catalogue.

In the app: the same calendar, with the same catalogue, shows under Agenda inside the clinic's workspace. Full guide in Manual → Clinic calendar.

Bring appointments into your own system announces every booking, every move and every cancellation, without your system asking every five minutes. Connect Claude to your calendar lets an AI client ask about the week, without writing anything.

On this page