VitrinaAPI

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.

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.

MethodPathWhat it does
GET/clinic/booking-draftsAbandoned bookings: what patients left on the details step
GET/clinic/booking-roster«Nueva cita» pickers from Vitrina (professionals + visit types)
POST/clinic/booking-roster/refreshRe-read the vendor's roster now («Actualizar»)
POST/clinic/budget-followups/{id}/outcomeLog the outcome of a follow-up call
GET/clinic/budget-followups/sin-hora«Llamar hoy» · «Sin hora»: open leads without a cita
POST/clinic/budget-followups/sin-hora/{lead_id}/outcomeLog the outcome of a call to a Sin hora lead
GET/clinic/budget-followups/today«Llamar hoy»: unaccepted presupuestos to follow up today
GET/clinic/diagnosticoThe Diagnóstico: day N of 30, live numbers, plan band
GET/clinic/evaluationThe clinic's «Evaluación»
PUT/clinic/evaluationUpdate the clinic's «Evaluación»
GET/clinic/insights/comercialCommercial numbers: presupuesto acceptance, cierres, cash, botados
GET/clinic/insights/ingresosIngresos: producción, presupuestado and recaudación by day
GET/clinic/insights/ingresos/days/{day}Ingresos: one day opened (its plans, payments and producción)
GET/clinic/insights/ingresos/monthsIngresos: doctor × month comparison
GET/clinic/insights/produccionWhat was performed and what was collected
GET/clinic/insights/resultados«Tu agenda con Vitrina»: what the AI did for the diary
GET/clinic/insights/resultados/people«Tu agenda con Vitrina»: the patients behind a count
GET/clinic/migration/runsThe clinic’s migration runs, newest first
GET/clinic/migration/runs/{id}One migration run: progress per family, gaps by reason
GET/clinic/targets/{month}The month's Meta mensual and its progress
PUT/clinic/targets/{month}Set the month's Meta mensual
GET/clinic/todayThe «Hoy» strip: is today on track?
GET/clinic/today/bookingsThe citas behind the «Hoy» numbers

GET /clinic/booking-drafts

Abandoned bookings: what patients left on the details step

One row per BOOKING DRAFT: a patient on a public booking surface (the hosted page or the clinic widget) reached the details step and typed a phone. status is open (not finished yet), completed (booked), recovered (booked after the WhatsApp recovery message went out — recovery_fired_at says when) or expired (never finished; the personal fields are scrubbed). consent_whatsapp is the unchecked-by-default box on the details step: the recovery Seguimiento never fires without it. attribution is the ad click the visit carried, which a recovered booking keeps. The resume token is never listed. Needs clinic:read and contacts:read (rows carry the phone typed).

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
statusqueryopen \completed \recovered \
limitqueryintegerno≥ 1, ≤ 200, por defecto 50
offsetqueryinteger | nullno≥ 0, ≤ 100000, por defecto 0
curl https://api.vitrinadev.com/api/v1/clinic/booking-drafts \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": [
    {
      "id": "6f1c2e8a-3b1d-4d5e-9a7b-1c2d3e4f5a6b",
      "status": "recovered",
      "surface": "booking_page",
      "landing_id": "0a9b8c7d-6e5f-4a3b-2c1d-0e9f8a7b6c5d",
      "service_id": "1b2c3d4e-5f6a-4b7c-8d9e-0f1a2b3c4d5e",
      "professional_id": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
      "starts_at": "2026-10-08T14:00:00.000Z",
      "name": "Ana Pérez",
      "phone": "+56912345678",
      "email": null,
      "consent_whatsapp": true,
      "attribution": {
        "utm_source": "facebook",
        "utm_campaign": "piso-pelvico-octubre",
        "fbclid": "IwAR2xYz",
        "surface": "booking_page",
        "captured_at": "2026-10-07T21:10:00.000Z"
      },
      "contact_id": "3d4e5f6a-7b8c-4d9e-0f1a-2b3c4d5e6f7a",
      "appointment_id": "4e5f6a7b-8c9d-4e0f-1a2b-3c4d5e6f7a8b",
      "recovery_fired_at": "2026-10-07T22:15:00.000Z",
      "completed_at": "2026-10-07T22:31:00.000Z",
      "created_at": "2026-10-07T21:10:00.000Z",
      "updated_at": "2026-10-07T22:31:00.000Z"
    }
  ],
  "meta": {
    "total": 1,
    "limit": 50,
    "offset": 0
  }
}

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

GET /clinic/booking-roster

«Nueva cita» pickers from Vitrina (professionals + visit types)

The active professionals and the bookable visit types of the clinic, read from Vitrina's own tables for every engine (never the vendor live). synced_at says how fresh the vendor roster is. Ids are the ones each engine's booking takes (see the schema).

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.

curl https://api.vitrinadev.com/api/v1/clinic/booking-roster \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "engine": "healthatom",
    "synced_at": "2026-10-23T12:00:00.000Z",
    "professionals": [
      {
        "id": "b6a1c2d3-0000-4000-8000-000000000001",
        "external_id": "12",
        "nombre": "Dra. Camila Rojas",
        "especialidad": "Ortodoncia",
        "especialidades": [
          "Ortodoncia"
        ],
        "sucursal_ids": [
          "1"
        ],
        "intervalo_minutes": 30,
        "agenda_online": true
      }
    ],
    "services": [
      {
        "id": "c7b2d3e4-0000-4000-8000-000000000002",
        "kind": "motivo",
        "external_id": "501",
        "nombre": "Evaluación de ortodoncia",
        "categoria": "Ortodoncia",
        "especialidad": "Ortodoncia",
        "duracion": 30,
        "profesionales": [
          "12"
        ],
        "duraciones": {
          "12": 30
        }
      }
    ]
  }
}

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

POST /clinic/booking-roster/refresh

Re-read the vendor's roster now («Actualizar»)

HealthAtom: re-reads the professionals roster now (done). Reservo: queues the account sync (queued). Native, or a refresh in the last minute: not_needed. Always answers with the roster as it stands.

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.

curl -X POST https://api.vitrinadev.com/api/v1/clinic/booking-roster/refresh \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "status": "done",
    "roster": {
      "engine": "healthatom",
      "synced_at": "2026-10-23T12:00:00.000Z",
      "professionals": [
        {
          "id": "b6a1c2d3-0000-4000-8000-000000000001",
          "external_id": "12",
          "nombre": "Dra. Camila Rojas",
          "especialidad": "Ortodoncia",
          "especialidades": [
            "Ortodoncia"
          ],
          "sucursal_ids": [
            "1"
          ],
          "intervalo_minutes": 30,
          "agenda_online": true
        }
      ],
      "services": [
        {
          "id": "c7b2d3e4-0000-4000-8000-000000000002",
          "kind": "motivo",
          "external_id": "501",
          "nombre": "Evaluación de ortodoncia",
          "categoria": "Ortodoncia",
          "especialidad": "Ortodoncia",
          "duracion": 30,
          "profesionales": [
            "12"
          ],
          "duraciones": {
            "12": 30
          }
        }
      ]
    }
  }
}

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

POST /clinic/budget-followups/{id}/outcome

Log the outcome of a follow-up call

One tap of reception, ledgered against the lineage. accepted and declined stop the follow-up (and cancel a touch waiting for approval); thinking skips today's WhatsApp; no_answer lets it go out this afternoon. A later tap on the same day supersedes the earlier one — a stop tapped by mistake is undone by tapping another outcome. Never accepts the presupuesto itself. 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
outcomeaccepted \declined \thinking \
curl -X POST https://api.vitrinadev.com/api/v1/clinic/budget-followups/<id>/outcome \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "outcome": "thinking"
  }'

Example response (200)

{
  "data": {
    "id": "7b7b7b7b-0000-4000-8000-000000000001",
    "lineage_id": "7c7c7c7c-0000-4000-8000-000000000001",
    "source": "native",
    "budget_id": "7d7d7d7d-0000-4000-8000-000000000001",
    "mirror_plan_id": null,
    "vendor": null,
    "budget_display_id": "P-0412",
    "patient_name": "María José Fuentes Lagos",
    "phone": "+56960000001",
    "contact_id": "1a73af9e-0000-4000-8000-000000000001",
    "clinic_patient_id": "b1b0b8de-0000-4000-8000-000000000001",
    "conversation_id": "c0c0c0c0-0000-4000-8000-000000000001",
    "linea": {
      "id": "d1d1d1d1-0000-4000-8000-000000000001",
      "nombre": "Implantes"
    },
    "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
    },
    "professional_name": "Dra. Ana Soto",
    "items": [
      "Implante unitario"
    ],
    "item_count": 1,
    "total_clp": 890000,
    "issued_on": "2026-10-08",
    "issued_label": "jueves 8 de octubre",
    "days_since_issue": 1,
    "step": 1,
    "status": "active",
    "stop_reason": null,
    "touches_sent": 0,
    "promised": false,
    "outcome": {
      "value": "thinking",
      "at": "2026-10-09T14:12:03.000Z"
    },
    "touch": null
  }
}

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

GET /clinic/budget-followups/sin-hora

«Llamar hoy» · «Sin hora»: open leads without a cita

Every OPEN lead whose contact has no cita that counts, however old (no age cut-off: dental and aesthetic demand is sporadic). A cita counts when it is confirmed or attended, or a hold still running; a cancelled or missed cita, past or future, does not — the lead is still waiting for a time. A lead leaves the list the moment a cita is booked. Sorted by call priority first (priority P1 to P4: P1 picked or asked for a time and was not booked, P2 wrote last and the clinic owes the reply, P3 interested, P4 cold), then lead score (highest first, unscored last), then the oldest last message first. Paged: limit (default 50, max 100) and cursor = the previous next_cursor; the cursor is a position, not an offset, so a lead leaving mid-scroll never skips another. counts covers the whole section, not the page: total, logged (an outcome tapped today) and unlogged. Each row carries what reception reads before dialling (patient_name, phone, score, score_reason, last_message_at) and today's outcome. line is the lead's Línea; origin is the lead's Origen (Meta with the ad, or Orgánico: the attribution Vitrina Ads reports). Narrow the section with origin=meta|organic|unknown, origin_ad_id and linea_id (a Línea id, or none for leads without one); counts follow the narrowing. Requires clinic:read and, for an API key or app, clinic_patients: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
limitqueryintegerno≥ 1, ≤ 100, por defecto 50
cursorquerystringno
originquerymeta \organic \unknown
origin_ad_idquerystringnopatrón ^[0-9]{1,32}$
linea_idqueryanyno
priorityquerystringnopatrón ^P[1-4](,P[1-4])*$
curl https://api.vitrinadev.com/api/v1/clinic/budget-followups/sin-hora \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "today": "2026-10-09",
    "call_list": true,
    "day_ends_at": "2026-10-09T22:00:00.000Z",
    "counts": {
      "total": 10,
      "logged": 0,
      "unlogged": 10,
      "by_priority": {
        "P1": 1,
        "P2": 2,
        "P3": 3,
        "P4": 4
      },
      "unlogged_by_priority": {
        "P1": 1,
        "P2": 2,
        "P3": 3,
        "P4": 4
      }
    },
    "rows": [
      {
        "priority": "P2",
        "priority_fact": "wrote_last",
        "priority_reason": "Escribió hace 3 h, le debemos respuesta",
        "id": "5e5e5e5e-0000-4000-8000-000000000001",
        "display_id": "L-89",
        "contact_id": "1a73af9e-0000-4000-8000-000000000001",
        "conversation_id": "c0c0c0c0-0000-4000-8000-000000000001",
        "patient_name": "María José Fuentes Lagos",
        "phone": "+56960000001",
        "score": 72,
        "score_reason": "Pidió precio de implantes y preguntó por horarios.",
        "temperature": "warm",
        "stage_name": "Nuevo",
        "stage_says_booked": false,
        "line": "Implantes",
        "linea": {
          "id": "d1d1d1d1-0000-4000-8000-000000000001",
          "nombre": "Implantes",
          "source": "keyword"
        },
        "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
        },
        "lead_created_at": "2026-09-20T15:02:11.000Z",
        "last_message_at": "2026-10-09T13:40:00.000Z",
        "days_since_last_message": 0,
        "outcome": null,
        "recontacto": null
      }
    ],
    "next_cursor": null,
    "viewer": {
      "may_log": true,
      "may_approve": true
    }
  }
}

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

POST /clinic/budget-followups/sin-hora/{lead_id}/outcome

Log the outcome of a call to a Sin hora lead

The same four one-tap outcomes as the presupuesto rows (accepted, declined, thinking, no_answer), recorded on the lead's timeline as a call_outcome activity. A later tap the same clinic day supersedes the earlier (the timeline keeps both). It records the call and cancels the WhatsApp proposed for this patient, if any. declined («No quiere») also closes the lead as Perdido: it moves to its pipeline's lost stage with «No quiere» as the reason, leaves Sin hora and is never chased by Recontacto again; moving it back to a stage undoes it. Returns the updated row, or null when the lead left the section (a cita was booked, or «No quiere»). A pipeline with no lost stage refuses declined. 404 when the lead is not open. 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
lead_idpathuuidyes

Body

FieldTypeRequiredConstraints
outcomeaccepted \declined \thinking \
curl -X POST https://api.vitrinadev.com/api/v1/clinic/budget-followups/sin-hora/<id>/outcome \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "outcome": "no_answer"
  }'

Example response (200)

{
  "data": {
    "priority": "P2",
    "priority_fact": "wrote_last",
    "priority_reason": "Escribió hace 3 h, le debemos respuesta",
    "id": "5e5e5e5e-0000-4000-8000-000000000001",
    "display_id": "L-89",
    "contact_id": "1a73af9e-0000-4000-8000-000000000001",
    "conversation_id": "c0c0c0c0-0000-4000-8000-000000000001",
    "patient_name": "María José Fuentes Lagos",
    "phone": "+56960000001",
    "score": 72,
    "score_reason": "Pidió precio de implantes y preguntó por horarios.",
    "temperature": "warm",
    "stage_name": "Nuevo",
    "stage_says_booked": false,
    "line": "Implantes",
    "linea": {
      "id": "d1d1d1d1-0000-4000-8000-000000000001",
      "nombre": "Implantes",
      "source": "keyword"
    },
    "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
    },
    "lead_created_at": "2026-09-20T15:02:11.000Z",
    "last_message_at": "2026-10-09T13:40:00.000Z",
    "days_since_last_message": 0,
    "outcome": {
      "value": "no_answer",
      "at": "2026-10-09T14:12:03.000Z"
    },
    "recontacto": null
  }
}

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

GET /clinic/budget-followups/today

«Llamar hoy»: unaccepted presupuestos to follow up today

The clinic's follow-up of unaccepted presupuestos, one row per LINEAGE (a re-issue never restarts it). A presupuesto is on the list on the clinic day after it was first sent, and again on day 7 and day 21 (step 1/2/3), until it is accepted, rejected, voided, expires (60 days, 90 for ortodoncia / implantología), a cita is booked, the patient answers a touch, or reception taps «Aceptó» / «No quiere». Rows carry what reception needs to call (patient, phone, items, total, professional) and the day's logged outcome. When no outcome was logged and nobody at the clinic wrote to the patient since the step opened, a WhatsApp touch is proposed at 17:00 clinic time (11:00 when call_list is off) — always for a person's approval (touch.action_id goes to POST /outbound/approvals/\{id\}/approve); at most three per lineage; never twice a day; never a price or a discount. counts.unlogged is «n sin registrar». summary covers the lineages issued in the last 30 days: followed up vs never contacted. template says whether the WhatsApp template for touches outside the 24 h window is approved. Each row carries the patient's origin (Meta with the ad, or Orgánico: the attribution Vitrina Ads reports); narrow the list with origin=meta|organic|unknown and origin_ad_id — counts follow the narrowing, summary does not. 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
originquerymeta \organic \unknown
origin_ad_idquerystringnopatrón ^[0-9]{1,32}$
curl https://api.vitrinadev.com/api/v1/clinic/budget-followups/today \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "today": "2026-10-09",
    "call_list": true,
    "template": {
      "name": "seguimiento_presupuesto",
      "status": "approved",
      "whatsapp_connected": true
    },
    "counts": {
      "to_call": 1,
      "logged": 0,
      "unlogged": 1,
      "by_priority": {
        "P1": 1,
        "P2": 2,
        "P3": 3,
        "P4": 4
      },
      "awaiting_approval": 0
    },
    "viewer": {
      "may_log": true,
      "may_approve": true,
      "may_configure": false
    },
    "money_visible": true,
    "rows": [
      {
        "id": "7b7b7b7b-0000-4000-8000-000000000001",
        "lineage_id": "7c7c7c7c-0000-4000-8000-000000000001",
        "source": "native",
        "budget_id": "7d7d7d7d-0000-4000-8000-000000000001",
        "mirror_plan_id": null,
        "vendor": null,
        "budget_display_id": "P-0412",
        "patient_name": "María José Fuentes Lagos",
        "phone": "+56960000001",
        "contact_id": "1a73af9e-0000-4000-8000-000000000001",
        "clinic_patient_id": "b1b0b8de-0000-4000-8000-000000000001",
        "conversation_id": "c0c0c0c0-0000-4000-8000-000000000001",
        "linea": {
          "id": "d1d1d1d1-0000-4000-8000-000000000001",
          "nombre": "Implantes"
        },
        "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
        },
        "professional_name": "Dra. Ana Soto",
        "items": [
          "Implante unitario"
        ],
        "item_count": 1,
        "total_clp": 890000,
        "issued_on": "2026-10-08",
        "issued_label": "jueves 8 de octubre",
        "days_since_issue": 1,
        "step": 1,
        "status": "active",
        "stop_reason": null,
        "touches_sent": 0,
        "promised": false,
        "outcome": null,
        "touch": null
      }
    ],
    "summary": {
      "from": "2026-09-10",
      "to": "2026-10-09",
      "lineages": 12,
      "followed_up": 9,
      "never_contacted": 3,
      "accepted_without_contact": 0,
      "in_progress": 4,
      "closed_after_followup": 5
    }
  }
}

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

GET /clinic/diagnostico

The Diagnóstico: day N of 30, live numbers, plan band

The clinic's free Diagnóstico in one read. status is not_started, active or closed; eligibility says whether the owner can start one and, if not, why (reason + a printable message; available_on when this clinic's yearly Diagnóstico was already used). window is the open window (else the newest closed one) with day of days_total, days_left, and closing.sentence — what the last day does: a window that turned the revenue mirror on stops it and deletes the copied detail unless the clinic holds the plan; a window over a mirror that was already on never deletes. report is the aggregate reading (the 90 days ending today, from the same engine as GET /clinic/insights/comercial) — live while the window is open, stored once closed; it carries the day-one plan band with its price. formula_status: propuesta marks the formulas the mentors have not frozen yet. history lists past Diagnósticos. Requires clinic_money:read or clinic_insights:read. Aggregates only — counts, CLP, rates, months and professional names; never a patient, cita, presupuesto or payment. A caller WITHOUT clinic_money:read (a Consultor or an app on the insights scope) additionally gets every figure resting on fewer than 5 cases withheld as null (suppression.min_count) and no deletion-ledger detail.

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.

curl https://api.vitrinadev.com/api/v1/clinic/diagnostico \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "status": "active",
    "viewer": {
      "may_start": true
    },
    "eligibility": {
      "can_start": false,
      "reason": "active",
      "message": "Tu Diagnóstico está en curso.",
      "available_on": null
    },
    "window": {
      "id": "0192f1c4-7d3a-7b21-9c4e-3f5a6b7c8d9e",
      "status": "active",
      "phase": "running",
      "closing_reason": null,
      "held_until": null,
      "started_at": "2026-10-05T13:00:00.000Z",
      "ends_at": "2026-11-04T13:00:00.000Z",
      "day": 12,
      "days_total": 30,
      "days_left": 19,
      "warned_at": null,
      "ended_early_at": null,
      "closed_at": null,
      "outcome": null,
      "mirror_origin": "diagnostico",
      "closing": {
        "deletes_detail": true,
        "sentence": "El 4 de noviembre dejamos de copiar datos y eliminamos el detalle copiado; conservamos solo el informe con tus totales. Con el plan, todo sigue al día."
      },
      "runs": []
    },
    "sync": {
      "state": "up_to_date",
      "synced_at": "2026-10-16T12:03:00.000Z",
      "message": null
    },
    "report": {
      "version": 1,
      "computed_at": "2026-10-16T12:10:00.000Z",
      "from": "2026-07-19",
      "to": "2026-10-16",
      "data_through": "2026-10-16T12:03:00.000Z",
      "state": "ready",
      "state_reason": null,
      "collected": {
        "clp": 48250000,
        "previous_clp": 44100000,
        "delta_pct": 9.4,
        "rule": "Lo cobrado en el período: todos los pagos recibidos, sin los anulados."
      },
      "acceptance": {
        "presented": 212,
        "accepted": 97,
        "rate": 45.8,
        "presented_clp": 186400000,
        "accepted_clp": 71900000,
        "formula_status": "acordada",
        "rule": "Presupuestos aceptados ÷ presupuestos presentados, cada presupuesto una vez."
      },
      "cierres": {
        "count": 81,
        "value_clp": 63300000,
        "formula_status": "acordada",
        "rule": "Un cierre es el primer pago de un presupuesto aceptado."
      },
      "by_professional": [
        {
          "professional_id": "0192f1c4-0000-7000-8000-000000000001",
          "professional_name": "Dra. Ejemplo",
          "presented": 64,
          "accepted": 35,
          "rate": 54.7,
          "accepted_clp": 28100000,
          "cierres": 30,
          "cierres_clp": 24900000,
          "suppressed": false
        }
      ],
      "months": [
        {
          "month": "2026-09",
          "accepted_clp": 24300000,
          "produced_clp": 19800000,
          "collected_clp": 16900000,
          "cierres": 27
        }
      ],
      "first_visit": {
        "patients": 140,
        "paid": 61,
        "rate": 43.6,
        "formula_status": "acordada",
        "rule": "De los pacientes nuevos cuya primera cita atendida cae en el período, la parte que ese día pagó más que la evaluación."
      },
      "second_visit": {
        "eligible": 79,
        "converted": 22,
        "rate": 27.8,
        "formula_status": "acordada",
        "rule": "De los presupuestos aceptados del período, la parte que se aceptó en una visita posterior."
      },
      "botados": {
        "unaccepted": {
          "count": 88,
          "clp": 74200000,
          "suppressed": false
        },
        "accepted_unscheduled": {
          "count": 0,
          "clp": 0,
          "suppressed": false
        },
        "mid_treatment_no_future_cita": {
          "count": 23,
          "clp": 12100000,
          "suppressed": false
        },
        "total": {
          "count": 111,
          "clp": 86300000,
          "suppressed": false
        },
        "focus": "unaccepted",
        "formula_status": "acordada",
        "rule": "Presupuestos de los últimos 12 meses sin cita futura: «nunca aceptó» o «quedó a medias» tras 90 días sin cita ni pago."
      },
      "band": {
        "band": "M",
        "count": 7.3,
        "has_history": true,
        "months": [
          {
            "month": "2026-07",
            "count": 7
          },
          {
            "month": "2026-08",
            "count": 8
          },
          {
            "month": "2026-09",
            "count": 7
          }
        ],
        "uf_month": 3,
        "estimated_clp_month": 119000,
        "price_label": "UF 3 al mes",
        "rule": "Tu tramo se revisa cada mes con el promedio de tus últimos 3 meses; si sube, te avisamos con un mes de anticipación."
      }
    },
    "report_source": "live",
    "plan": {
      "holds_plan": false,
      "status": "none"
    },
    "history": [],
    "suppression": null
  }
}

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

GET /clinic/evaluation

The clinic's «Evaluación»

Clinic data the per-agent price policy reads: whether the clinic charges the evaluation (declared: false while it never said), a single amount (null = each specialty's evaluation prestación price, summarised in default_amount), whether it is waived when the patient starts treatment, and the convenios with a free evaluation. It reaches an agent's prompt only when that agent's behavior_policies.treatment_price is set. 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.

curl https://api.vitrinadev.com/api/v1/clinic/evaluation \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "declared": true,
    "charged": true,
    "amount_clp": null,
    "waived_on_treatment_start": true,
    "free_agreements": [
      {
        "id": "7c1e2f4a-0000-4000-8000-000000000001",
        "name": "Convenio Club Deportivo"
      }
    ],
    "default_amount": {
      "min_clp": 15000,
      "max_clp": 50000,
      "services": [
        {
          "id": "5d2b9e10-0000-4000-8000-000000000001",
          "nombre": "Evaluación medicina estética facial",
          "categoria": null,
          "precio_clp": 15000
        },
        {
          "id": "5d2b9e10-0000-4000-8000-000000000002",
          "nombre": "Evaluación y diagnóstico dental inicial",
          "categoria": null,
          "precio_clp": 50000
        }
      ],
      "service_count": 2
    },
    "agreements": [
      {
        "id": "7c1e2f4a-0000-4000-8000-000000000001",
        "name": "Convenio Club Deportivo"
      }
    ]
  }
}

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

PUT /clinic/evaluation

Update the clinic's «Evaluación»

Partial: merges field by field over what is stored. free_agreement_ids must be convenios of this workspace (400 otherwise). Returns the same view as the GET. Audited. Requires clinic_admin: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.

Body

FieldTypeRequiredConstraints
chargedboolean—
amount_clpinteger | null—≥ 0, ≤ 100000000
waived_on_treatment_startboolean—
free_agreement_idsuuid[]—
curl -X PUT https://api.vitrinadev.com/api/v1/clinic/evaluation \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "charged": true,
    "waived_on_treatment_start": true
  }'

Example response (200)

{
  "data": {
    "declared": true,
    "charged": true,
    "amount_clp": null,
    "waived_on_treatment_start": true,
    "free_agreements": [
      {
        "id": "7c1e2f4a-0000-4000-8000-000000000001",
        "name": "Convenio Club Deportivo"
      }
    ],
    "default_amount": {
      "min_clp": 15000,
      "max_clp": 50000,
      "services": [
        {
          "id": "5d2b9e10-0000-4000-8000-000000000001",
          "nombre": "Evaluación medicina estética facial",
          "categoria": null,
          "precio_clp": 15000
        },
        {
          "id": "5d2b9e10-0000-4000-8000-000000000002",
          "nombre": "Evaluación y diagnóstico dental inicial",
          "categoria": null,
          "precio_clp": 50000
        }
      ],
      "service_count": 2
    },
    "agreements": [
      {
        "id": "7c1e2f4a-0000-4000-8000-000000000001",
        "name": "Convenio Club Deportivo"
      }
    ]
  }
}

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

GET /clinic/insights/comercial

Commercial numbers: presupuesto acceptance, cierres, cash, botados

The clinic's commercial numbers, aggregates only (counts, CLP, percentages, months and professional names; never a patient). regime says how acceptance is read: evidencia when the revenue mirror is on (a non-entry line performed, or a cita booked after the presupuesto, inside its window), explicita otherwise (the acceptance the clinic recorded). Acceptance is by COUNT of presupuestos, one per lineage; $0 and entry-only presupuestos are out of both sides; the window is 60 days (90 for ortodoncia and implantología); the rate goes to the professional who issued the presupuesto. Presupuestos ISSUED in the window form the acceptance cohort (cohorts by issue month with 30- and 60-day reads). A cierre is the first payment on an accepted presupuesto: once per presupuesto, dated on that payment, valued at the full accepted value. headline is the cash collected in the window against the previous window of the same length. first_visit (new patients who paid more than the evaluación the day of their first attended cita; null with reason when no evaluación is priced), second_visit (accepted presupuestos captured after the issue day) and botados (two lists — «nunca aceptó» and «quedó a medias», 90 days without cita or pago and no future cita, over the presupuestos of the last 12 months, per issuing professional) carry formula: mentores_2026_09_29. An acceptance proven by a cita stays accepted when the cita is later cancelled (cita_only counts them). acceptance.reference and each by_professional[].reference place the rate on the mentors' «rango de referencia del programa» (the dentist's especialidad range, else general; null when none). Every KPI carries its formula and a printable rule; a rate with no denominator is null, never 0. state is empty, settling (presupuesto lines still being read; state_reason says so) or ready. Requires clinic_money:read (or clinic_insights:read, which reads the same aggregates). Accepts from / to (bare YYYY-MM-DD, to INCLUSIVE; default the 90 days ending today in the clinic's zone), location_id and professional_id. With professional_id, cash (headline, months[].collected_clp) is null: a payment belongs to the patient, not to a professional. A caller WITHOUT clinic_money:read always reads one fixed window — the last 3 closed calendar months — whatever range or filter it asks (suppression.filters_ignored), with every count or CLP resting on 1–4 cases null, rates over fewer than 5 cases null, complementary suppression over professionals, cohorts and botados lists, and month rows carrying only cash.

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
fromquerystringnopatrón ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$
toquerystringnopatrón ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$
location_idqueryuuidno
professional_idqueryuuidno
curl https://api.vitrinadev.com/api/v1/clinic/insights/comercial \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "from": "2026-06-01",
    "to": "2026-08-29",
    "timezone": "America/Santiago",
    "location_id": null,
    "professional_id": null,
    "state": "ready",
    "state_reason": null,
    "regime": "evidencia",
    "regime_label": "Aceptación por evidencia",
    "regime_rule": "Leído desde tu software de agenda: un presupuesto cuenta como aceptado cuando se realizó una de sus prestaciones o el paciente quedó con una cita agendada después de recibirlo, aunque después la cancele.",
    "source": {
      "kind": "mirror",
      "synced_at": "2026-08-30T03:10:00.000Z"
    },
    "formulas": {
      "acceptance": "mentores_2026_09_25",
      "cierre": "cierre_2026_09_25",
      "first_visit": "mentores_2026_09_29",
      "second_visit": "mentores_2026_09_29",
      "botados": "mentores_2026_09_29"
    },
    "headline": {
      "collected_clp": 48500000,
      "previous_collected_clp": 44100000,
      "delta_pct": 10,
      "previous_from": "2026-03-03",
      "previous_to": "2026-05-31",
      "rule": "Lo cobrado en el período: todos los pagos recibidos, sin los anulados. Se compara con el período anterior del mismo largo."
    },
    "acceptance": {
      "presented": 420,
      "accepted": 231,
      "rate": 55,
      "same_day": 118,
      "later": 113,
      "cita_only": 14,
      "pending": 36,
      "presented_clp": 142000000,
      "accepted_clp": 71300000,
      "rate_30d": 47.2,
      "rate_60d": 53.8,
      "formula": "mentores_2026_09_25",
      "window_days": 60,
      "long_window_days": 90,
      "rule": "Presupuestos aceptados ÷ presupuestos presentados, cada presupuesto una vez.",
      "reference": {
        "label": "Rango de referencia del programa",
        "especialidad": null,
        "band": "malo",
        "note": null,
        "muy_malo_upper_pct": 40,
        "malo_upper_pct": 60,
        "regular_upper_pct": 70,
        "excelente_lower_pct": 90
      }
    },
    "cierres": {
      "count": 180,
      "value_clp": 58900000,
      "accepted_without_payment": 51,
      "formula": "cierre_2026_09_25",
      "rule": "Un cierre es el primer pago de un presupuesto aceptado."
    },
    "cohorts": [
      {
        "month": "2026-06",
        "presented": 140,
        "accepted": 80,
        "rate": 57.1,
        "same_day": 41,
        "later": 39,
        "cita_only": 2,
        "pending": 0,
        "presented_clp": 47000000,
        "accepted_clp": 24800000,
        "rate_30d": 49.3,
        "rate_60d": 56.4,
        "mature": true
      }
    ],
    "by_professional": [
      {
        "professional_id": "0f3c2a9e-5b1d-4c7e-9a42-6d8e1f2b3c4d",
        "professional_name": "Dra. Camila Soto",
        "presented": 96,
        "accepted": 61,
        "rate": 63.5,
        "same_day": 30,
        "later": 31,
        "cita_only": 3,
        "pending": 8,
        "presented_clp": 33400000,
        "accepted_clp": 19900000,
        "cierres": 47,
        "cierres_clp": 15100000,
        "reference": {
          "label": "Rango de referencia del programa",
          "especialidad": null,
          "band": "regular",
          "note": null,
          "muy_malo_upper_pct": 40,
          "malo_upper_pct": 60,
          "regular_upper_pct": 70,
          "excelente_lower_pct": 90
        }
      }
    ],
    "unassigned_presented": 0,
    "unassigned_cierres": 0,
    "months": [
      {
        "month": "2026-06",
        "accepted_clp": 24100000,
        "produced_clp": 19700000,
        "collected_clp": 16200000,
        "cierres": 62,
        "cierres_clp": 20300000
      }
    ],
    "first_visit": {
      "formula": "mentores_2026_09_29",
      "patients": 210,
      "paid": 97,
      "rate": 46.2,
      "evaluation_price_clp": 34990,
      "reason": null,
      "rule": "De los pacientes nuevos cuya primera cita atendida cae en el período, la parte que ese día pagó más que la evaluación ($34.990)."
    },
    "second_visit": {
      "formula": "mentores_2026_09_29",
      "eligible": 231,
      "converted": 113,
      "rate": 48.9,
      "rule": "De los presupuestos aceptados del período, la parte que se aceptó en una visita posterior y no el mismo día en que se entregó."
    },
    "botados": {
      "unaccepted": {
        "count": 88,
        "clp": 31200000
      },
      "accepted_unscheduled": {
        "count": 0,
        "clp": 0
      },
      "mid_treatment_no_future_cita": {
        "count": 23,
        "clp": 9800000
      },
      "by_professional": [
        {
          "professional_id": "0f3c2a9e-5b1d-4c7e-9a42-6d8e1f2b3c4d",
          "professional_name": "Dra. Camila Soto",
          "unaccepted": {
            "count": 21,
            "clp": 7400000
          },
          "mid_treatment_no_future_cita": {
            "count": 6,
            "clp": 2900000
          }
        }
      ],
      "formula": "mentores_2026_09_29",
      "basis": "live_last_12_months",
      "idle_days": 90,
      "total": {
        "count": 111,
        "clp": 41000000
      },
      "focus": "unaccepted",
      "rule": "Presupuestos de los últimos 12 meses sin cita futura: «nunca aceptó» o «quedó a medias» tras 90 días sin cita ni pago."
    },
    "coverage": {
      "plans_without_lines": 0,
      "excluded_zero": 301,
      "excluded_entry_only": 92
    }
  }
}

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

GET /clinic/insights/ingresos

Ingresos: producción, presupuestado and recaudación by day

«Ingresos» (D56). A NATIVE clinic reads Vitrina's own ledger (source.kind: native, #5367): producción = the service lines of attended citas by the cita's day, presupuestado = presupuesto lineages first sent that day at the latest version's total, recaudación = live payments by fecha de pago, acceptance = the folio's explicit acceptance. A Dentalink / Medilink clinic with the revenue mirror on reads the mirror (source.kind: mirror): producción (performed plan lines by fecha de realización), presupuestado (plans issued, header total, evaluación included, as the vendor's panel) and recaudación (live payments by fecha de recepción). Anything else is state: unavailable. Each flow per day is split nuevos (the patient's primera visita starts in that month) vs tratamiento. hoy carries today's three numbers whatever the window. by_professional: producción by PERFORMER; presupuestos, aceptación and monto aceptado by the QUOTING professional under Números comerciales' acceptance rule ($0 and evaluación-only presupuestos out); abonado = paid so far on the plans issued in the period; new patients presented / accepted. Default window: the current month to today. With professional_id recaudación is null (a payment names no professional). Every day and the totals also carry by_origen (D77): the three flows split Meta / Orgánico / Sin dato by the shared person-origin resolver; origin narrows the whole report to one Origen. Requires clinic_money:read, or clinic_insights:read (the Consultor seat, D76: the payload is aggregates only). Accepts from / to (bare YYYY-MM-DD, to INCLUSIVE), location_id and professional_id. The default window is the 90 days ending today in the clinic's own zone. A professional_id that is not yours is a 404, never an empty report. Also reachable with clinic_insights:read — the aggregates-only «Consultor» role (spec #2441 D11) — in addition to the scope named above. A caller WITHOUT the report’s own scope never receives a patient-identifiable row (inasistencias.citas and packs.expiring come back empty with aggregate_only: true and rows_withheld naming the field) and reads the minimum-count floor: always the last 3 closed calendar months with no professional or sucursal filter, whatever was asked (suppression.filters_ignored); a count or CLP resting on 1–4 cases is null; a rate over fewer than 5 cases, or whose numerator is withheld, is null; list rows that add up to a shown total get complementary suppression and leave the list (suppression.withheld_rows); per-professional/prestación/weekday outcome rows and per-channel confirmation rows carry rates only; producción serves no per-prestación rows (they nest inside the categorías); clinic-wide cash over whole months stays while its slices are floored. suppression.min_count is the floor.

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
fromquerystringnopatrón ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$
toquerystringnopatrón ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$
location_idqueryuuidno
professional_idqueryuuidno
originquerymeta \organic \unknown
curl https://api.vitrinadev.com/api/v1/clinic/insights/ingresos \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "from": "2026-10-01",
    "to": "2026-10-08",
    "today": "2026-10-08",
    "timezone": "America/Santiago",
    "location_id": null,
    "professional_id": null,
    "origin": null,
    "state": "ready",
    "state_reason": null,
    "source": {
      "kind": "mirror",
      "synced_at": "2026-10-08T20:10:00.000Z"
    },
    "rules": {
      "produccion": "Lo realizado en el box …",
      "presupuestado": "Presupuestos creados en el día …",
      "recaudacion": "Pagos recibidos en el día …",
      "nuevos": "Paciente nuevo: su primera visita es en ese mismo mes …",
      "aceptacion": "Aceptados con la misma regla de Números comerciales …",
      "abonado": "Lo ya pagado a los presupuestos creados en el período …"
    },
    "hoy": {
      "day": "2026-10-08",
      "produccion_clp": 3670982,
      "prestaciones": 47,
      "presupuestado_clp": 4381028,
      "presupuestos": 18,
      "recaudado_clp": 3307992,
      "pagos": 21,
      "citas": 52
    },
    "totals": {
      "produccion": {
        "total_clp": 18626657,
        "nuevos_clp": 412000,
        "tratamiento_clp": 3258982,
        "prestaciones": 47
      },
      "presupuestado": {
        "total_clp": 25014395,
        "nuevos_clp": 1072572,
        "tratamiento_clp": 3308456,
        "presupuestos": 18
      },
      "recaudacion": {
        "total_clp": 14856381,
        "nuevos_clp": 811084,
        "tratamiento_clp": 2496908,
        "pagos": 21
      },
      "by_origen": {
        "meta": {
          "produccion_clp": 1204000,
          "presupuestado_clp": 1830000,
          "recaudado_clp": 1020000
        },
        "organic": {
          "produccion_clp": 2210982,
          "presupuestado_clp": 2301028,
          "recaudado_clp": 1987992
        },
        "unknown": {
          "produccion_clp": 256000,
          "presupuestado_clp": 250000,
          "recaudado_clp": 300000
        }
      }
    },
    "days": [
      {
        "day": "2026-10-08",
        "produccion": {
          "total_clp": 3670982,
          "nuevos_clp": 412000,
          "tratamiento_clp": 3258982,
          "prestaciones": 47
        },
        "presupuestado": {
          "total_clp": 4381028,
          "nuevos_clp": 1072572,
          "tratamiento_clp": 3308456,
          "presupuestos": 18
        },
        "recaudacion": {
          "total_clp": 3307992,
          "nuevos_clp": 811084,
          "tratamiento_clp": 2496908,
          "pagos": 21
        },
        "by_origen": {
          "meta": {
            "produccion_clp": 1204000,
            "presupuestado_clp": 1830000,
            "recaudado_clp": 1020000
          },
          "organic": {
            "produccion_clp": 2210982,
            "presupuestado_clp": 2301028,
            "recaudado_clp": 1987992
          },
          "unknown": {
            "produccion_clp": 256000,
            "presupuestado_clp": 250000,
            "recaudado_clp": 300000
          }
        }
      }
    ],
    "clinic": {
      "produccion_clp": 18626657,
      "prestaciones": 52,
      "presupuestos": 14,
      "presupuestado_clp": 5120000,
      "aceptados": 9,
      "aceptacion_pct": 64.3,
      "aceptado_clp": 3010000,
      "abonado_clp": 1240000,
      "nuevos_presentados": 5,
      "nuevos_aceptados": 3
    },
    "by_professional": [
      {
        "professional_id": "6b1f3c2e-8a4d-4f0e-9c1b-2d7e5a9f0c31",
        "professional_name": "Ana Rojas",
        "produccion_clp": 3685671,
        "prestaciones": 52,
        "presupuestos": 14,
        "presupuestado_clp": 5120000,
        "aceptados": 9,
        "aceptacion_pct": 64.3,
        "aceptado_clp": 3010000,
        "abonado_clp": 1240000,
        "nuevos_presentados": 5,
        "nuevos_aceptados": 3
      }
    ]
  }
}

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

GET /clinic/insights/ingresos/days/{day}

Ingresos: one day opened (its plans, payments and producción)

The plans issued and payments received on one clinic day, each marked nuevo / tratamiento, plans with the Números comerciales verdict, plus producción per performer. Patient names only for a caller allowed to see identity (contacts:read + clinic:read, plus clinic_patients:read for an API credential); otherwise patient.name is null. Each row carries the Origen of its patient (origen). Requires clinic_money:read, or clinic_insights:read, which never gets a patient reference (patient.id and patient.name are null).

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
daypathstringyespatrón ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$
curl https://api.vitrinadev.com/api/v1/clinic/insights/ingresos/days/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "day": "2026-10-08",
    "identity": true,
    "plans": [
      {
        "plan_id": "1d2c3b4a-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
        "patient": {
          "id": "7e6d5c4b-3a29-4180-9f8e-7d6c5b4a3928",
          "name": "Paciente de ejemplo"
        },
        "professional_id": "6b1f3c2e-8a4d-4f0e-9c1b-2d7e5a9f0c31",
        "professional_name": "Ana Rojas",
        "total_clp": 640000,
        "abonado_clp": 120000,
        "deuda_clp": 520000,
        "tipo": "nuevo",
        "origen": "meta",
        "aceptado": true
      }
    ],
    "payments": [
      {
        "payment_id": "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
        "time": "11:42",
        "patient": {
          "id": "7e6d5c4b-3a29-4180-9f8e-7d6c5b4a3928",
          "name": "Paciente de ejemplo"
        },
        "amount_clp": 120000,
        "tipo": "nuevo",
        "origen": "meta"
      }
    ],
    "produccion": [
      {
        "professional_id": "6b1f3c2e-8a4d-4f0e-9c1b-2d7e5a9f0c31",
        "professional_name": "Ana Rojas",
        "prestaciones": 9,
        "produccion_clp": 812000
      }
    ]
  }
}

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

GET /clinic/insights/ingresos/months

Ingresos: doctor × month comparison

The last months calendar months (default 6, at most 12, the current one to today): per month the clinic's producción, recaudación, presupuestos, aceptación and monto aceptado; per doctor the same by month (producción by performer, the rest by the quoting professional over the presupuestos issued that month). Requires clinic_money:read or clinic_insights:read (aggregates only).

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
monthsqueryintegerno≥ 1, ≤ 12
location_idqueryuuidno
curl https://api.vitrinadev.com/api/v1/clinic/insights/ingresos/months \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "from": "2026-09-01",
    "to": "2026-10-08",
    "state": "ready",
    "rules": {
      "produccion": "Lo realizado en el box …",
      "presupuestado": "Presupuestos creados en el día …",
      "recaudacion": "Pagos recibidos en el día …",
      "nuevos": "Paciente nuevo: su primera visita es en ese mismo mes …",
      "aceptacion": "Aceptados con la misma regla de Números comerciales …",
      "abonado": "Lo ya pagado a los presupuestos creados en el período …"
    },
    "months": [
      {
        "month": "2026-09",
        "produccion_clp": 57179516,
        "recaudacion_clp": 57172232,
        "presupuestos": 278,
        "presupuestado_clp": 112745314,
        "aceptacion_pct": 63.1,
        "aceptado_clp": 51093178
      }
    ],
    "professionals": [
      {
        "professional_id": "6b1f3c2e-8a4d-4f0e-9c1b-2d7e5a9f0c31",
        "professional_name": "Ana Rojas",
        "months": [
          {
            "month": "2026-09",
            "produccion_clp": 9623135,
            "presupuestos": 41,
            "presupuestado_clp": 18200000,
            "aceptacion_pct": 68.3,
            "aceptado_clp": 9100000
          }
        ]
      }
    ]
  }
}

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

GET /clinic/insights/produccion

What was performed and what was collected

Producción, aggregates only. A Dentalink / Medilink clinic (revenue mirror on, basis: mirror_lines) reads the vendor's own plan lines marked realizado, by fecha de realización, at the line's total — Dentalink's «Ventas (acciones realizadas)», evaluación included, $0 lines out; collected_clp is every live payment received in the window (null with professional_id: a payment names no professional). Otherwise only ATTENDED citas count, valued from the cita's frozen clinic_appointment_service lines (a native clinic) or the catálogo's current price for the prestación the mirror names; lines_without_price names the hole, and collected_clp is what the ledger allocated to those citas. by_professional and the professional_id filter follow who PERFORMED the work, not who quoted the plan. Requires clinic_money:read; a caller holding only clinic_insights:read reads the last closed months with the minimum-count floor (suppression). Accepts from / to (bare YYYY-MM-DD, to INCLUSIVE), location_id and professional_id. The default window is the 90 days ending today in the clinic's own zone. A professional_id that is not yours is a 404, never an empty report. Also reachable with clinic_insights:read — the aggregates-only «Consultor» role (spec #2441 D11) — in addition to the scope named above. A caller WITHOUT the report’s own scope never receives a patient-identifiable row (inasistencias.citas and packs.expiring come back empty with aggregate_only: true and rows_withheld naming the field) and reads the minimum-count floor: always the last 3 closed calendar months with no professional or sucursal filter, whatever was asked (suppression.filters_ignored); a count or CLP resting on 1–4 cases is null; a rate over fewer than 5 cases, or whose numerator is withheld, is null; list rows that add up to a shown total get complementary suppression and leave the list (suppression.withheld_rows); per-professional/prestación/weekday outcome rows and per-channel confirmation rows carry rates only; producción serves no per-prestación rows (they nest inside the categorías); clinic-wide cash over whole months stays while its slices are floored. suppression.min_count is the floor.

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
fromquerystringnopatrón ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$
toquerystringnopatrón ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$
location_idqueryuuidno
professional_idqueryuuidno
curl https://api.vitrinadev.com/api/v1/clinic/insights/produccion \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "from": "2026-09-01",
    "to": "2026-09-30",
    "location_id": null,
    "professional_id": null,
    "basis": "mirror_lines",
    "totals": {
      "attended": 612,
      "lines": 699,
      "visits": 431,
      "lines_without_price": 0,
      "performed_clp": 57179516,
      "collected_clp": 57172232,
      "collected_basis": "mirror_payment.received"
    },
    "by_professional": [
      {
        "key": "00000000-0000-4000-8000-0000000000a1",
        "label": "Ana Rojas",
        "appointments": 141,
        "lines": 168,
        "performed_clp": 9623135
      }
    ],
    "by_service": [
      {
        "key": "00000000-0000-4000-8000-0000000000b3",
        "label": "Endodoncia molar",
        "appointments": 38,
        "lines": 38,
        "performed_clp": 9880000
      }
    ],
    "by_category": [
      {
        "key": "Endodoncia",
        "label": "Endodoncia",
        "appointments": 52,
        "lines": 61,
        "performed_clp": 12400000
      }
    ],
    "months": [
      {
        "month": "2026-09",
        "attended": 612,
        "performed_clp": 57179516,
        "collected_clp": 57172232
      }
    ]
  }
}

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

GET /clinic/insights/resultados

«Tu agenda con Vitrina»: what the AI did for the diary

One canonical read of what Vitrina's agent did for the clinic's agenda, aggregates only (never a patient, cita or conversation id). A cita is «agendada por Vitrina» when the agent booked it in a conversation (source = 'agent' and no public booking page); the window is on the day the cita HAPPENS (starts_at), blocks and holds out. Blocks: bookings (count and share of the agenda, with the previous period of the same length); money (the value of those citas — pipeline — and the CASH their patients paid in the period from the day Vitrina booked them, read from the revenue mirror when it is on, else from the native ledger; null with withheld: true for a caller without clinic_money:read); out_of_hours (bookings made while the workspace was closed per its business hours, plus a weekday × hour heatmap; null when no hours are configured); autonomy (bookings with no team message in the conversation, from the patient's first message of the 7 days before); time_to_book (median and p95, first patient message → booking); hours_returned (AI bookings × 10 minutes, a stated rule); recovery (no-shows over known attendance, and no-shows / cancellations the same patient booked again within 30 days, with how many of those Vitrina booked); confirmation (attendance of confirmed vs unconfirmed citas, the confirmaciones report's own figures); ads (cost per cita agendada / asistida / paciente que pagó = the Ads overview's spend ÷ its counts for the same dates; not_active without Vitrina Ads); reminders (reminders that did not go out, by cause). Every block carries a printable rule; every rate is null — never 0 — without a denominator, and measured figures carry measured / unknown counts. Requires clinic:read. Accepts from / to (bare YYYY-MM-DD, to inclusive; default the 90 days ending today in the clinic's zone) and location_id.

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
fromquerystringnopatrón ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$
toquerystringnopatrón ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$
location_idqueryuuidno
curl https://api.vitrinadev.com/api/v1/clinic/insights/resultados \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "from": "2026-07-01",
    "to": "2026-09-28",
    "timezone": "America/Santiago",
    "location_id": null,
    "family": "clinic",
    "previous_period": {
      "from": "2026-04-02",
      "to": "2026-06-30"
    },
    "bookings": {
      "ai": 286,
      "agenda_total": 2011,
      "share_pct": 14.2,
      "previous": {
        "ai": 240,
        "agenda_total": 1950,
        "share_pct": 12.3
      },
      "rule": "Citas del período (por la fecha de la cita) que agendó el asistente de Vitrina en una conversación, ÷ todas las citas del período."
    },
    "money": {
      "applies": true,
      "withheld": false,
      "booked_clp": 11400000,
      "booked_priced": 271,
      "booked_unpriced": 15,
      "agenda_clp": 96000000,
      "clp_share_pct": 11.9,
      "paid_clp": 7250000,
      "paid_patients": 164,
      "unlinked": 4,
      "regime": "mirror",
      "previous": {
        "booked_clp": 9800000,
        "paid_clp": 6100000
      },
      "rule_booked": "Valor de las citas que agendó Vitrina: el precio de sus prestaciones, o el del catálogo. Es lo agendado, no lo cobrado.",
      "rule_paid": "Pagos recibidos en el período por los pacientes que Vitrina agendó, desde el día en que los agendó."
    },
    "out_of_hours": {
      "configured": true,
      "count": 111,
      "pct": 38.8,
      "previous_pct": 36.1,
      "heatmap": [
        {
          "weekday": 1,
          "hour": 21,
          "count": 9
        },
        {
          "weekday": 6,
          "hour": 11,
          "count": 14
        }
      ],
      "rule": "Citas que Vitrina agendó mientras tu negocio estaba cerrado, según tu horario de atención (incluye feriados)."
    },
    "autonomy": {
      "no_human": 158,
      "pct": 56.6,
      "measured": 279,
      "unknown": 7,
      "previous_pct": 52,
      "rule": "Citas de Vitrina en las que nadie de tu equipo escribió en la conversación antes de agendar."
    },
    "time_to_book": {
      "median_seconds": 734,
      "p95_seconds": 80100,
      "p95_phrase": "5 de cada 100 personas tardan más de 22 h 15 min en quedar agendadas.",
      "measured": 279,
      "unknown": 7,
      "previous_median_seconds": 910,
      "rule": "Tiempo desde el primer mensaje del paciente hasta que su cita quedó agendada (la mediana)."
    },
    "hours_returned": {
      "minutes_per_booking": 10,
      "hours": 47.7,
      "rule": "Cada cita que agenda Vitrina ahorra 10 minutos de responder y agendar a mano."
    },
    "recovery": {
      "window_days": 30,
      "known_attendance": 1063,
      "unknown_attendance": 21,
      "no_show_pct": 10.2,
      "no_show": {
        "count": 108,
        "recovered": 37,
        "recovered_via_vitrina": 17,
        "recovered_pct": 34.6,
        "via_vitrina_pct": 15.9,
        "pending": 12,
        "unlinked": 1
      },
      "cancelled": {
        "count": 240,
        "recovered": 26,
        "recovered_via_vitrina": 11,
        "recovered_pct": 10.8,
        "via_vitrina_pct": 4.6,
        "pending": 18,
        "unlinked": 0
      },
      "rule_no_show": "Citas pasadas marcadas como «no asistió» ÷ citas con asistencia conocida (asistió + no asistió).",
      "rule_recovered": "Una cita perdida se recupera cuando el mismo paciente agenda otra dentro de 30 días."
    },
    "confirmation": {
      "applies": true,
      "confirmed_attendance_pct": 93.1,
      "unconfirmed_attendance_pct": 78.4,
      "confirmed_closed": 610,
      "unconfirmed_closed": 212,
      "rule": "Asistencia de las citas cuyo paciente confirmó el recordatorio, frente a las que nunca respondieron."
    },
    "ads": {
      "state": "active",
      "spend_clp": 1648000,
      "booked": 58,
      "attended": 41,
      "paid": 22,
      "cost_per_booked": 28414,
      "cost_per_attended": 40195,
      "cost_per_paid": 74909,
      "rule": "Inversión en anuncios del período ÷ citas agendadas, asistidas y pacientes de anuncios que pagaron, según Vitrina Ads para las mismas fechas."
    },
    "reminders": {
      "applies": true,
      "sent": 721,
      "failed": 120,
      "failed_pct": 14.3,
      "by_cause": [
        {
          "cause": "no_whatsapp",
          "count": 61
        },
        {
          "cause": "no_phone",
          "count": 38
        },
        {
          "cause": "meta_payment_block",
          "count": 21
        }
      ],
      "rule": "Recordatorios de citas del período que no salieron, agrupados por la causa que registró el envío."
    },
    "truncated": false
  }
}

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

GET /clinic/insights/resultados/people

«Tu agenda con Vitrina»: the patients behind a count

The citas or patients behind ONE count of GET /clinic/insights/resultados, read through the same window read — so count equals the page's number and people has exactly count rows. metric: ai_bookings (bookings.ai, one row per cita), out_of_hours (out_of_hours.count, one row per cita; count: null without business hours), paid_patients (money.paid_patients, one row per patient with every payment since Vitrina booked them; needs clinic_money:read, else withheld: true and no list). Each row: the contact and its roster patient (patient_link, patient.\{id,name,rut,phone\} — the identity gate of /ads/attributed-sales: contacts:read + clinic:read, plus clinic_patients:read for an API credential), and the cita (start, booking instant, status, professional, out-of-hours). Requires clinic:read. Accepts the page's from / to / location_id.

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
fromquerystringnopatrón ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$
toquerystringnopatrón ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$
location_idqueryuuidno
metricqueryai_bookings \out_of_hours \paid_patients
curl https://api.vitrinadev.com/api/v1/clinic/insights/resultados/people \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "metric": "paid_patients",
    "from": "2026-09-08",
    "to": "2026-10-07",
    "location_id": null,
    "count": 1,
    "people": [
      {
        "key": "7d1c2a5e-0000-4000-8000-000000000001",
        "patient_link": "linked",
        "patient": {
          "id": "7d1c2a5e-0000-4000-8000-000000000001",
          "name": "María Ejemplo Soto",
          "rut": "11.111.111-1",
          "phone": "+56911111111"
        },
        "contact": {
          "id": "3f9b6c10-0000-4000-8000-000000000002",
          "display_name": "María Ejemplo",
          "key": "0002"
        },
        "cita": {
          "id": "a0c4e2b1-0000-4000-8000-000000000003",
          "display_id": null,
          "starts_at": "2026-09-15T13:00:00.000Z",
          "booked_at": "2026-09-11T23:40:00.000Z",
          "status": "completed",
          "professional": {
            "id": "b5d7f9a3-0000-4000-8000-000000000004",
            "name": "Dra. Ejemplo"
          },
          "out_of_hours": true
        },
        "paid": {
          "total": 45000,
          "count": 1,
          "since_day": "2026-09-11",
          "payments": [
            {
              "day": "2026-09-15",
              "amount": 45000
            }
          ]
        }
      }
    ],
    "truncated": false,
    "withheld": false,
    "people_state": "ready"
  }
}

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

GET /clinic/migration/runs

The clinic’s migration runs, newest first

The clinic’s migration runs, newest first

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
limitqueryintegerno≥ 1, ≤ 100, por defecto 20
curl https://api.vitrinadev.com/api/v1/clinic/migration/runs \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "items": [
      {
        "id": "01929a5e-7c10-7d3e-9a41-5b8f2c6d1e07",
        "mode": "apply",
        "status": "completed",
        "status_label": "Completada",
        "families": [
          "patients",
          "appointments"
        ],
        "started_at": "2026-10-30T23:05:00.000Z",
        "finished_at": "2026-10-31T01:42:10.000Z",
        "totals": {
          "examined": 4213,
          "created": 3980,
          "unchanged": 190,
          "gaps": 43
        }
      }
    ]
  }
}

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

GET /clinic/migration/runs/{id}

One migration run: progress per family, gaps by reason

Per family: status, phase, counts (examined / created / converted / updated / unchanged / kept_native / collapsed / skipped / gaps), progress against the latest dry-run, vendor requests. The gap summary counts what could not come across by reason, with Spanish labels. Counts and reason codes only — never a patient.

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/migration/runs/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "id": "01929a5e-7c10-7d3e-9a41-5b8f2c6d1e07",
    "mode": "apply",
    "status": "completed",
    "status_label": "Completada",
    "families": [
      {
        "family": "patients",
        "label": "Fichas administrativas de pacientes (con cargas)",
        "status": "done",
        "status_label": "Lista",
        "reason": null,
        "reason_label": null,
        "phase": null,
        "phase_label": null,
        "counts": {
          "examined": 1480,
          "created": 1402,
          "unchanged": 60,
          "gaps": 18
        },
        "progress": {
          "examined": 1480,
          "expected": 1480
        },
        "vendor_requests": 31,
        "failures": 0,
        "started_at": "2026-10-30T23:05:02.000Z",
        "finished_at": "2026-10-30T23:31:40.000Z"
      }
    ],
    "totals": {
      "examined": 1480,
      "created": 1402,
      "unchanged": 60,
      "gaps": 18
    },
    "gaps": [
      {
        "family": "patients",
        "reason": "native_rut_match",
        "reason_label": "Ya existe en Vitrina una ficha creada aquí con el mismo RUT: no se duplicó ni se fusionó.",
        "count": 18
      }
    ],
    "history_from": "2021-01-01",
    "window_ends_at": "2026-11-01T21:00:00.000Z",
    "started_at": "2026-10-30T23:05:00.000Z",
    "finished_at": "2026-10-30T23:31:40.000Z",
    "last_progress_at": "2026-10-30T23:31:40.000Z",
    "job_deleted_at": "2026-10-30T23:31:41.000Z",
    "instruction": {
      "version": "clinic-migration-instruction/2026-09-28",
      "sha256": "b1946ac92492d2347c6235b4d2611184b1946ac92492d2347c6235b4d2611184",
      "text": "Instruyo a Vitrina copiar desde Dentalink las fichas administrativas de pacientes …",
      "instructed_by_user_id": "20000000-0000-4000-8000-000000000001",
      "instructed_at": "2026-10-30T23:04:40.000Z",
      "document_reference": null
    },
    "error": null
  }
}

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

GET /clinic/targets/{month}

The month's Meta mensual and its progress

Requires clinic_money:read or clinic_insights:read (the Consultor seat: the payload is aggregates only, D76). The owner's four targets for the month (primeras visitas booked, cierres, cobrado = recaudación in CLP, producción in CLP; null = not set) with what has been achieved so far: primeras visitas = citas created in the month whose Tipo de agendamiento is Nuevo or Nuevo · reagenda; cierres as Números comerciales counts them; cobrado = payments received; producción = performed plan lines by fecha de realización, as «Ingresos» (null without the revenue mirror). For the current month, pace carries the ritmo diario and, per figure, week_need («esta semana hay que generar») and projection (the end-of-month projection at the current pace: gap = projected − target). The ritmo diario is a catch-up pace: (meta − achieved before today) ÷ working days left, today included, each day weighed by its opening hours against the clinic's longest day (a 9–14 Saturday next to 9–19 weekdays weighs 0.5), times today's weight (per_day). Working days come from the clinic's opening hours and holidays, in the clinic's timezone. A figure with no target has a count and no pace. This is not the Ads goal, which is marketing's own.

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
monthpathstringyespatrón ^\d{4}-(0[1-9]|1[0-2])$
curl https://api.vitrinadev.com/api/v1/clinic/targets/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "month": "2026-10",
    "timezone": "America/Santiago",
    "targets": {
      "month": "2026-10",
      "primeras_visitas": 40,
      "cierres": 12,
      "cobrado_clp": 18000000,
      "produccion_clp": 60000000,
      "updated_at": "2026-10-01T13:05:00.000Z",
      "updated_by": null
    },
    "achieved": {
      "primeras_visitas": 18,
      "cierres": 5,
      "cobrado_clp": 7450000,
      "produccion_clp": 21300000
    },
    "working_days": {
      "total": 21,
      "left": 10
    },
    "history": [
      {
        "date": "2026-10-01",
        "booked": 108,
        "primeras_visitas": 26,
        "plata_clp": 1251950
      },
      {
        "date": "2026-10-02",
        "booked": 49,
        "primeras_visitas": 11,
        "plata_clp": 359086
      }
    ],
    "pace": {
      "primeras_visitas": {
        "status": "open",
        "target": 40,
        "achieved_before_today": 18,
        "remaining": 22,
        "working_days_left": 10,
        "weighted_days_left": 9.5,
        "day_weight": 1,
        "per_day": 3,
        "week_need": 12,
        "projection": {
          "projected": 36,
          "gap": -4,
          "meets_target": false
        }
      },
      "cierres": null,
      "cobrado_clp": {
        "status": "open",
        "target": 18000000,
        "achieved_before_today": 7450000,
        "remaining": 10550000,
        "working_days_left": 10,
        "weighted_days_left": 9.5,
        "day_weight": 1,
        "per_day": 1110527,
        "week_need": 5550000,
        "projection": {
          "projected": 15900000,
          "gap": -2100000,
          "meets_target": false
        }
      },
      "produccion_clp": {
        "status": "open",
        "target": 60000000,
        "achieved_before_today": 21300000,
        "remaining": 38700000,
        "working_days_left": 10,
        "weighted_days_left": 9.5,
        "day_weight": 1,
        "per_day": 3927000,
        "week_need": 19600000,
        "projection": {
          "projected": 52400000,
          "gap": -7600000,
          "meets_target": false
        }
      }
    }
  }
}

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

PUT /clinic/targets/{month}

Set the month's Meta mensual

Requires clinic_admin:write. Replaces the month's targets: a figure left out or null is cleared (except produccion_clp, which keeps its value when left out). Answers with the same targets and progress as the GET.

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

Body

FieldTypeRequiredConstraints
primeras_visitasinteger | null—≥ 0, ≤ 100000
cierresinteger | null—≥ 0, ≤ 100000
cobrado_clpinteger | null—≥ 0, ≤ 1000000000000
produccion_clpinteger | null—≥ 0, ≤ 1000000000000
curl -X PUT https://api.vitrinadev.com/api/v1/clinic/targets/<id> \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "primeras_visitas": 40,
    "cierres": 12,
    "cobrado_clp": 18000000,
    "produccion_clp": 60000000
  }'

Example response (200)

{
  "data": {
    "month": "2026-10",
    "timezone": "America/Santiago",
    "targets": {
      "month": "2026-10",
      "primeras_visitas": 40,
      "cierres": 12,
      "cobrado_clp": 18000000,
      "produccion_clp": 60000000,
      "updated_at": "2026-10-01T13:05:00.000Z",
      "updated_by": null
    },
    "achieved": {
      "primeras_visitas": 18,
      "cierres": 5,
      "cobrado_clp": 7450000,
      "produccion_clp": 21300000
    },
    "working_days": {
      "total": 21,
      "left": 10
    },
    "history": [
      {
        "date": "2026-10-01",
        "booked": 108,
        "primeras_visitas": 26,
        "plata_clp": 1251950
      },
      {
        "date": "2026-10-02",
        "booked": 49,
        "primeras_visitas": 11,
        "plata_clp": 359086
      }
    ],
    "pace": {
      "primeras_visitas": {
        "status": "open",
        "target": 40,
        "achieved_before_today": 18,
        "remaining": 22,
        "working_days_left": 10,
        "weighted_days_left": 9.5,
        "day_weight": 1,
        "per_day": 3,
        "week_need": 12,
        "projection": {
          "projected": 36,
          "gap": -4,
          "meets_target": false
        }
      },
      "cierres": null,
      "cobrado_clp": {
        "status": "open",
        "target": 18000000,
        "achieved_before_today": 7450000,
        "remaining": 10550000,
        "working_days_left": 10,
        "weighted_days_left": 9.5,
        "day_weight": 1,
        "per_day": 1110527,
        "week_need": 5550000,
        "projection": {
          "projected": 15900000,
          "gap": -2100000,
          "meets_target": false
        }
      },
      "produccion_clp": {
        "status": "open",
        "target": 60000000,
        "achieved_before_today": 21300000,
        "remaining": 38700000,
        "working_days_left": 10,
        "weighted_days_left": 9.5,
        "day_weight": 1,
        "per_day": 3927000,
        "week_need": 19600000,
        "projection": {
          "projected": 52400000,
          "gap": -7600000,
          "meets_target": false
        }
      }
    }
  }
}

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

GET /clinic/today

The «Hoy» strip: is today on track?

Requires clinic:read or clinic_money:read; cobrado figures need clinic_money:read and are null without it. Bookings made today (citas created today, for any date; an anulación is not a booking; a moved cita stays a booking of the day it was made, and its replacement counts on the day it is re-booked) with their Tipo de agendamiento judged at the moment of booking, the bookings of the last hour, the same counts for the previous working day, cobrado today and the part from new patients (patients with a primera visita this month; null when the clinic's vendor has no payment mirror, e.g. Reservo), and the paces of today's month. The ritmo diario is a catch-up pace: (meta − achieved before today) ÷ working days left, today included, each day weighed by its opening hours against the clinic's longest day (a 9–14 Saturday next to 9–19 weekdays weighs 0.5), times today's weight (per_day). Working days come from the clinic's opening hours and holidays, in the clinic's timezone. A figure with no target has a count and no pace. The pace of cobrado is the Meta mensual's, set against the clinic's total cobrado.

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.

curl https://api.vitrinadev.com/api/v1/clinic/today \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "as_of": "2026-10-13T18:30:00.000Z",
    "timezone": "America/Santiago",
    "today": {
      "date": "2026-10-13",
      "is_working_day": true,
      "booked": {
        "total": 9,
        "primeras_visitas": 4,
        "by_kind": {
          "new": 3,
          "new_rebooked": 1,
          "in_treatment": 5
        }
      },
      "last_hour": {
        "total": 2,
        "primeras_visitas": 1
      },
      "by_hour": [
        {
          "hour": 9,
          "total": 3,
          "primeras_visitas": 1,
          "by_kind": {
            "new": 1,
            "new_rebooked": 0,
            "in_treatment": 2
          }
        },
        {
          "hour": 11,
          "total": 4,
          "primeras_visitas": 2,
          "by_kind": {
            "new": 1,
            "new_rebooked": 1,
            "in_treatment": 2
          }
        },
        {
          "hour": 14,
          "total": 2,
          "primeras_visitas": 1,
          "by_kind": {
            "new": 1,
            "new_rebooked": 0,
            "in_treatment": 1
          }
        }
      ],
      "origin": {
        "meta": 2,
        "organic": 1,
        "unknown": 1
      },
      "from_whatsapp": {
        "total": 3,
        "primeras_visitas": 2
      },
      "cobrado_clp": 640000,
      "cobrado_new_patients_clp": 90000
    },
    "previous_working_day": {
      "date": "2026-10-09",
      "booked": {
        "total": 11,
        "primeras_visitas": 3,
        "by_kind": {
          "new": 2,
          "new_rebooked": 1,
          "in_treatment": 8
        }
      },
      "cobrado_clp": 810000,
      "cobrado_new_patients_clp": 120000
    },
    "pace": {
      "month": "2026-10",
      "working_days_left": 14,
      "primeras_visitas": {
        "status": "open",
        "target": 40,
        "achieved_before_today": 18,
        "remaining": 22,
        "working_days_left": 10,
        "weighted_days_left": 9.5,
        "day_weight": 1,
        "per_day": 3
      },
      "cobrado_clp": null
    }
  }
}

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

GET /clinic/today/bookings

The citas behind the «Hoy» numbers

Requires clinic:read. The citas booked on one clinic day (date, default today; never a future day): when they were booked (the vendor's own creation time when the mirror carries it — Reservo — else when Vitrina first saw the cita, which is every Dentalink / Medilink cita), the hour of that in the clinic's clock, who the patient is, for when, with which professional, the Tipo de agendamiento judged as of the booking day, the Origen, whether the patient's linked contact had WhatsApp activity from two days before the booking day («Desde WhatsApp»), and the patient's last attended visit before that day. An anulación is not a booking; a moved cita («Cambio de fecha») stays a booking of the day it was made.

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}$
curl https://api.vitrinadev.com/api/v1/clinic/today/bookings \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "date": "2026-10-13",
    "timezone": "America/Santiago",
    "rows": [
      {
        "appointment_id": "6f1c2c1e-1d2b-4c9a-9f3e-0a1b2c3d4e5f",
        "booked_at": "2026-10-13T14:12:00.000Z",
        "hour": 11,
        "starts_at": "2026-10-20T13:00:00.000Z",
        "patient": {
          "id": "0d9e8f7a-6b5c-4d3e-2f1a-0b9c8d7e6f5a",
          "name": "Paciente Ejemplo"
        },
        "professional_name": "Dra. Ejemplo",
        "kind": "new",
        "origin": null,
        "from_whatsapp": true,
        "last_visit": null
      }
    ]
  }
}

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

On this page