Packs de sesiones
Sesiones vendidas por adelantado: el pack del catálogo, la compra de un paciente y su saldo derivado.
Descarga la proyección completa de la API pública: openapi.json.
Definir prestaciones y precios explica este recurso en prosa, con ejemplos ejecutables.
| Método | Ruta | Qué hace |
|---|---|---|
GET | /clinic/packs | List pack definitions (the catalog) |
POST | /clinic/packs | Define a pack (the catalog row) |
DELETE | /clinic/packs/{id} | Retire a pack |
PATCH | /clinic/packs/{id} | Edit a pack |
POST | /clinic/packs/purchases | Sell a pack to a patient |
GET | /clinic/packs/purchases/{id} | One pack purchase, with its balance and its ledger state |
POST | /clinic/packs/purchases/{id}/cancel | Anular or refund a pack purchase |
POST | /clinic/packs/purchases/{id}/consume | Consume one session |
POST | /clinic/packs/purchases/{id}/freeze | Freeze a pack |
GET | /clinic/packs/purchases/{id}/sessions | One pack purchase's session ledger |
POST | /clinic/packs/purchases/{id}/sessions/{sessionId}/reverse | Undo a consumption |
POST | /clinic/packs/purchases/{id}/unfreeze | Lift a freeze |
GET | /clinic/patients/{id}/packs | A patient's pack balances |
GET /clinic/packs
List pack definitions (the catalog)
What the clinic sells as a multi-session pack: session count, price (bigint whole CLP, null when not recorded — never $0), validity, cadence and freeze policy. Reservo "Planes" mirror in as source: reservo. active omitted returns every row.
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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
active | query | true \ | false | no |
page | query | integer | no | ≥ 1, por defecto 1 |
limit | query | integer | no | ≥ 1, ≤ 200, por defecto 50 |
curl https://api.vitrinadev.com/api/v1/clinic/packs \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"data": [
{
"id": "6acd6212-0000-4000-8000-000000000001",
"source": "native",
"external_id": null,
"name": "Pack 4 controles de ortodoncia",
"description": null,
"session_count": 4,
"precio_clp": 96000,
"validity_days": 180,
"cadence_days": null,
"freeze_allowed": true,
"freeze_max_count": 1,
"transferable": false,
"per_session_forfeits_price": true,
"requires_full_prepayment": true,
"online_bookable": false,
"active": true,
"synced_at": null,
"items": [
{
"id": "3a7d9767-0000-4000-8000-000000000001",
"clinic_service_id": "4cf5bc59-0000-4000-8000-000000000001",
"quantity": 4,
"source": "native"
}
]
}
],
"total": 1,
"page": 1,
"limit": 50
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /clinic/packs
Define a pack (the catalog row)
What the clinic sells as a multi-session product: sessions, price (bigint whole CLP), vigencia, cadencia, freeze policy and the prestaciones it covers. items is HETEROGENEOUS — a pack of 4 limpiezas + 2 controles is two entries, which is how "tratamientos agrupados" are expressed rather than built twice.
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.
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
name | string | sí | mín. 1, máx. 200 |
description | string | null | — | máx. 2000 |
session_count | integer | sí | ≥ 1, ≤ 999 |
precio_clp | integer | null | — | ≥ 0, ≤ 999999999999 |
validity_days | integer | null | — | ≥ 1, ≤ 3650 |
cadence_days | integer | null | — | ≥ 1, ≤ 365 |
freeze_allowed | boolean | — | |
freeze_max_count | integer | — | ≥ 0, ≤ 10 |
transferable | boolean | — | |
per_session_forfeits_price | boolean | — | |
requires_full_prepayment | boolean | — | |
online_bookable | boolean | — | |
active | boolean | — | |
items | object[] | — |
curl -X POST https://api.vitrinadev.com/api/v1/clinic/packs \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Pack 4 controles de ortodoncia",
"session_count": 4,
"precio_clp": 96000,
"validity_days": 180,
"freeze_allowed": true,
"freeze_max_count": 1,
"items": [
{
"clinic_service_id": "4cf5bc59-0000-4000-8000-000000000001",
"quantity": 4
}
]
}'Ejemplo de respuesta (201)
{
"data": {
"id": "6acd6212-0000-4000-8000-000000000001",
"source": "native",
"external_id": null,
"name": "Pack 4 controles de ortodoncia",
"description": null,
"session_count": 4,
"precio_clp": 96000,
"validity_days": 180,
"cadence_days": null,
"freeze_allowed": true,
"freeze_max_count": 1,
"transferable": false,
"per_session_forfeits_price": true,
"requires_full_prepayment": true,
"online_bookable": false,
"active": true,
"synced_at": null,
"items": [
{
"id": "3a7d9767-0000-4000-8000-000000000001",
"clinic_service_id": "4cf5bc59-0000-4000-8000-000000000001",
"quantity": 4,
"source": "native"
}
]
}
}Responde: 201 · 400 · 401 · 403 · 404 · 409 · 429
DELETE /clinic/packs/{id}
Retire a pack
Deactivates; never deletes. A sold purchase still points at the row and the reports have to keep naming what was sold last year.
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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
curl -X DELETE https://api.vitrinadev.com/api/v1/clinic/packs/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"id": "6acd6212-0000-4000-8000-000000000002",
"source": "native",
"external_id": null,
"name": "Pack de prueba",
"description": null,
"session_count": 2,
"precio_clp": null,
"validity_days": null,
"cadence_days": null,
"freeze_allowed": false,
"freeze_max_count": 1,
"transferable": false,
"per_session_forfeits_price": true,
"requires_full_prepayment": true,
"online_bookable": false,
"active": false,
"synced_at": null,
"items": []
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
PATCH /clinic/packs/{id}
Edit a pack
A MIRRORED pack (Reservo's Planes) keeps the vendor's name, price, session count and vigencia — those four are overwritten on the next sync, so editing them here is refused out loud. The POLICY columns (freeze rules, transferability, online bookability) are Vitrina-owned and editable on any row.
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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
name | string | — | mín. 1, máx. 200 |
description | string | null | — | máx. 2000 |
session_count | integer | — | ≥ 1, ≤ 999 |
precio_clp | integer | null | — | ≥ 0, ≤ 999999999999 |
validity_days | integer | null | — | ≥ 1, ≤ 3650 |
cadence_days | integer | null | — | ≥ 1, ≤ 365 |
freeze_allowed | boolean | — | |
freeze_max_count | integer | — | ≥ 0, ≤ 10 |
transferable | boolean | — | |
per_session_forfeits_price | boolean | — | |
requires_full_prepayment | boolean | — | |
online_bookable | boolean | — | |
active | boolean | — | |
items | object[] | — |
curl -X PATCH https://api.vitrinadev.com/api/v1/clinic/packs/<id> \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"description": "Cuatro controles mensuales, válidos seis meses"
}'Ejemplo de respuesta (200)
{
"data": {
"id": "6acd6212-0000-4000-8000-000000000001",
"source": "native",
"external_id": null,
"name": "Pack 4 controles de ortodoncia",
"description": "Cuatro controles mensuales, válidos seis meses",
"session_count": 4,
"precio_clp": 96000,
"validity_days": 180,
"cadence_days": null,
"freeze_allowed": true,
"freeze_max_count": 1,
"transferable": false,
"per_session_forfeits_price": true,
"requires_full_prepayment": true,
"online_bookable": false,
"active": true,
"synced_at": null,
"items": [
{
"id": "3a7d9767-0000-4000-8000-000000000001",
"clinic_service_id": "4cf5bc59-0000-4000-8000-000000000001",
"quantity": 4,
"source": "native"
}
]
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /clinic/packs/purchases
Sell a pack to a patient
Writes BOTH halves of a pack: the ENTITLEMENT (sessions, vigencia resolved once as a date) here, and the MONEY as one obligation_group of kind pack plus one pack obligation in the payments ledger, linked by obligation_group_id. The price comes from the catalog, from the pricing engine (use_quote) or from an explicit precio_clp — which REQUIRES price_override_reason. The response carries the purchase, its DERIVED balance and its DERIVED ledger state; no total is stored anywhere.
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.
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
clinic_patient_id | string | null | — | |
contact_id | string | null | — | |
pack_id | string | null | — | |
session_count | integer | null | — | ≥ 1, ≤ 999 |
name | string | null | — | mín. 1, máx. 200 |
precio_clp | integer | null | — | ≥ 0, ≤ 999999999999 |
price_override_reason | string | null | — | mín. 3, máx. 500 |
use_quote | boolean | — | |
expires_at | string | null | — | patrón ^\d{4}-\d{2}-\d{2}$ |
due_on | string | null | — | patrón ^\d{4}-\d{2}-\d{2}$ |
curl -X POST https://api.vitrinadev.com/api/v1/clinic/packs/purchases \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"clinic_patient_id": "b1b0b8de-0000-4000-8000-000000000001",
"contact_id": "1a73af9e-0000-4000-8000-000000000001",
"pack_id": "6acd6212-0000-4000-8000-000000000001"
}'Ejemplo de respuesta (201)
{
"data": {
"purchase": {
"id": "d376ca29-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"clinic_patient_id": "b1b0b8de-0000-4000-8000-000000000001",
"pack_id": "6acd6212-0000-4000-8000-000000000001",
"budget_id": null,
"obligation_group_id": "a067dce6-0000-4000-8000-000000000001",
"name_snapshot": "Pack 4 controles de ortodoncia",
"session_count": 4,
"precio_clp": 96000,
"purchased_at": "2026-09-22T22:21:14.782Z",
"expires_at": "2027-03-21",
"frozen_from": null,
"frozen_to": null,
"freeze_count": 0,
"status": "active",
"cancelled_reason": null,
"price_source": "catalog",
"price_override_reason": null,
"price_explain": [],
"sold_by": null,
"source": "native",
"external_id": null,
"data": {},
"synced_at": null,
"created_at": "2026-09-22T22:21:14.781Z",
"updated_at": "2026-09-22T22:21:14.784Z"
},
"balance": {
"session_count": 4,
"consumed": 0,
"reversed": 0,
"remaining": 4,
"over_consumed": false,
"frozen": false,
"date_passed": false,
"days_to_expiry": 180,
"effective_status": "active"
},
"today": "2026-09-22",
"ledger": {
"id": "a067dce6-0000-4000-8000-000000000001",
"contact_id": "1a73af9e-0000-4000-8000-000000000001",
"kind": "pack",
"label": "Pack 4 controles de ortodoncia",
"status": "open",
"obligation_count": 1,
"expected_clp": 96000,
"allocated_clp": 0,
"outstanding_clp": 96000,
"state": "pending",
"obligations": [
{
"id": "b44936d0-0000-4000-8000-000000000001",
"contact_id": "1a73af9e-0000-4000-8000-000000000001",
"kind": "pack",
"direction": "receivable",
"label": "Pack 4 controles de ortodoncia",
"expected_clp": 96000,
"allocated_clp": 0,
"outstanding_clp": 96000,
"overpaid_clp": 0,
"state": "pending",
"provenance": null,
"is_overdue": false,
"status": "open",
"disposition": null,
"opened_on": "2026-09-22",
"due_on": null,
"closed_at": null,
"closed_reason": null,
"group_id": "a067dce6-0000-4000-8000-000000000001",
"source": "staff",
"document": null,
"allocations": []
}
],
"created_at": "2026-09-22T22:21:14.760Z"
}
}
}Responde: 201 · 400 · 401 · 403 · 404 · 409 · 429
GET /clinic/packs/purchases/{id}
One pack purchase, with its balance and its ledger state
Sessions remaining derived from the session rows, money derived from the ledger. Nothing on this response is a stored total.
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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
curl https://api.vitrinadev.com/api/v1/clinic/packs/purchases/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"purchase": {
"id": "d376ca29-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"clinic_patient_id": "b1b0b8de-0000-4000-8000-000000000001",
"pack_id": "6acd6212-0000-4000-8000-000000000001",
"budget_id": null,
"obligation_group_id": "a067dce6-0000-4000-8000-000000000001",
"name_snapshot": "Pack 4 controles de ortodoncia",
"session_count": 4,
"precio_clp": 96000,
"purchased_at": "2026-09-22T22:21:14.782Z",
"expires_at": "2027-03-21",
"frozen_from": null,
"frozen_to": null,
"freeze_count": 0,
"status": "active",
"cancelled_reason": null,
"price_source": "catalog",
"price_override_reason": null,
"price_explain": [],
"sold_by": null,
"source": "native",
"external_id": null,
"data": {},
"synced_at": null,
"created_at": "2026-09-22T22:21:14.781Z",
"updated_at": "2026-09-22T22:21:14.784Z"
},
"balance": {
"session_count": 4,
"consumed": 0,
"reversed": 0,
"remaining": 4,
"over_consumed": false,
"frozen": false,
"date_passed": false,
"days_to_expiry": 180,
"effective_status": "active"
},
"today": "2026-09-22",
"ledger": {
"id": "a067dce6-0000-4000-8000-000000000001",
"contact_id": "1a73af9e-0000-4000-8000-000000000001",
"kind": "pack",
"label": "Pack 4 controles de ortodoncia",
"status": "open",
"obligation_count": 1,
"expected_clp": 96000,
"allocated_clp": 0,
"outstanding_clp": 96000,
"state": "pending",
"obligations": [
{
"id": "b44936d0-0000-4000-8000-000000000001",
"contact_id": "1a73af9e-0000-4000-8000-000000000001",
"kind": "pack",
"direction": "receivable",
"label": "Pack 4 controles de ortodoncia",
"expected_clp": 96000,
"allocated_clp": 0,
"outstanding_clp": 96000,
"overpaid_clp": 0,
"state": "pending",
"provenance": null,
"is_overdue": false,
"status": "open",
"disposition": null,
"opened_on": "2026-09-22",
"due_on": null,
"closed_at": null,
"closed_reason": null,
"group_id": "a067dce6-0000-4000-8000-000000000001",
"source": "staff",
"document": null,
"allocations": []
}
],
"created_at": "2026-09-22T22:21:14.760Z"
}
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /clinic/packs/purchases/{id}/cancel
Anular or refund a pack purchase
Marks the entitlement cancelled or refunded with a written reason. The session rows are NOT deleted (they are evidence of visits that happened) and no pesos move: voiding the money is the ledger's own act on the obligation.
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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
reason | string | sí | mín. 3, máx. 500 |
refunded | boolean | — |
curl -X POST https://api.vitrinadev.com/api/v1/clinic/packs/purchases/<id>/cancel \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"reason": "El paciente desistió antes de la primera sesión",
"refunded": false
}'Ejemplo de respuesta (200)
{
"data": {
"purchase": {
"id": "d376ca29-0000-4000-8000-000000000002",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"clinic_patient_id": "42db9ec9-0000-4000-8000-000000000001",
"pack_id": "6acd6212-0000-4000-8000-000000000001",
"budget_id": null,
"obligation_group_id": null,
"name_snapshot": "Pack 4 controles de ortodoncia",
"session_count": 4,
"precio_clp": 96000,
"purchased_at": "2026-09-22T22:21:14.882Z",
"expires_at": "2027-03-21",
"frozen_from": null,
"frozen_to": null,
"freeze_count": 0,
"status": "cancelled",
"cancelled_reason": "El paciente desistió antes de la primera sesión",
"price_source": "catalog",
"price_override_reason": null,
"price_explain": [],
"sold_by": null,
"source": "native",
"external_id": null,
"data": {},
"synced_at": null,
"created_at": "2026-09-22T22:21:14.881Z",
"updated_at": "2026-09-22T22:21:14.888Z"
},
"balance": {
"session_count": 4,
"consumed": 0,
"reversed": 0,
"remaining": 4,
"over_consumed": false,
"frozen": false,
"date_passed": false,
"days_to_expiry": 180,
"effective_status": "cancelled"
},
"today": "2026-09-22",
"ledger": null
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /clinic/packs/purchases/{id}/consume
Consume one session
Appends a clinic_pack_session row. Refused when nothing remains, when the pack expired and when it is frozen — each of those three passable with a written override_reason, and none of them passable on a cancelled or refunded pack. One live consumption per (purchase, cita), enforced by a unique index: the same cita consumes once however many times it is closed.
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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
appointment_id | string | null | — | |
clinic_appointment_service_id | string | null | — | |
clinic_service_id | string | null | — | |
professional_id | string | null | — | |
consumed_at | string | null | — | date-time |
note | string | null | — | máx. 500 |
override_reason | string | null | — | mín. 3, máx. 500 |
curl -X POST https://api.vitrinadev.com/api/v1/clinic/packs/purchases/<id>/consume \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"clinic_service_id": "4cf5bc59-0000-4000-8000-000000000001",
"professional_id": "18342d1b-0000-4000-8000-000000000001",
"note": "Control 1 de 4"
}'Ejemplo de respuesta (201)
{
"data": {
"session": {
"id": "fcbdc4c2-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"pack_purchase_id": "d376ca29-0000-4000-8000-000000000001",
"appointment_id": null,
"clinic_appointment_service_id": null,
"clinic_service_id": "4cf5bc59-0000-4000-8000-000000000001",
"professional_id": "18342d1b-0000-4000-8000-000000000001",
"consumed_at": "2026-09-22T22:21:14.813Z",
"reversal_of": null,
"note": "Control 1 de 4",
"override_reason": null,
"created_by": null,
"source": "native",
"external_id": null,
"data": {},
"synced_at": null,
"created_at": "2026-09-22T22:21:14.812Z"
},
"view": {
"purchase": {
"id": "d376ca29-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"clinic_patient_id": "b1b0b8de-0000-4000-8000-000000000001",
"pack_id": "6acd6212-0000-4000-8000-000000000001",
"budget_id": null,
"obligation_group_id": "a067dce6-0000-4000-8000-000000000001",
"name_snapshot": "Pack 4 controles de ortodoncia",
"session_count": 4,
"precio_clp": 96000,
"purchased_at": "2026-09-22T22:21:14.782Z",
"expires_at": "2027-03-21",
"frozen_from": null,
"frozen_to": null,
"freeze_count": 0,
"status": "active",
"cancelled_reason": null,
"price_source": "catalog",
"price_override_reason": null,
"price_explain": [],
"sold_by": null,
"source": "native",
"external_id": null,
"data": {},
"synced_at": null,
"created_at": "2026-09-22T22:21:14.781Z",
"updated_at": "2026-09-22T22:21:14.784Z"
},
"balance": {
"session_count": 4,
"consumed": 1,
"reversed": 0,
"remaining": 3,
"over_consumed": false,
"frozen": false,
"date_passed": false,
"days_to_expiry": 180,
"effective_status": "active"
},
"today": "2026-09-22",
"ledger": {
"id": "a067dce6-0000-4000-8000-000000000001",
"contact_id": "1a73af9e-0000-4000-8000-000000000001",
"kind": "pack",
"label": "Pack 4 controles de ortodoncia",
"status": "open",
"obligation_count": 1,
"expected_clp": 96000,
"allocated_clp": 0,
"outstanding_clp": 96000,
"state": "pending",
"obligations": [
{
"id": "b44936d0-0000-4000-8000-000000000001",
"contact_id": "1a73af9e-0000-4000-8000-000000000001",
"kind": "pack",
"direction": "receivable",
"label": "Pack 4 controles de ortodoncia",
"expected_clp": 96000,
"allocated_clp": 0,
"outstanding_clp": 96000,
"overpaid_clp": 0,
"state": "pending",
"provenance": null,
"is_overdue": false,
"status": "open",
"disposition": null,
"opened_on": "2026-09-22",
"due_on": null,
"closed_at": null,
"closed_reason": null,
"group_id": "a067dce6-0000-4000-8000-000000000001",
"source": "staff",
"document": null,
"allocations": []
}
],
"created_at": "2026-09-22T22:21:14.760Z"
}
}
}
}Responde: 201 · 400 · 401 · 403 · 404 · 409 · 429
POST /clinic/packs/purchases/{id}/freeze
Freeze a pack
Honours the catalog's freeze_allowed / freeze_max_count (a clinic's bridal package may be configured never to freeze). A CLOSED window pushes expires_at out by its own length immediately; an OPEN one («hasta nuevo aviso») pays out when it is lifted, because its length is not known until then.
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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
from | string | null | — | patrón ^\d{4}-\d{2}-\d{2}$ |
to | string | null | — | patrón ^\d{4}-\d{2}-\d{2}$ |
reason | string | null | — | máx. 500 |
curl -X POST https://api.vitrinadev.com/api/v1/clinic/packs/purchases/<id>/freeze \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "2026-10-01",
"to": "2026-10-31",
"reason": "Viaje"
}'Ejemplo de respuesta (200)
{
"data": {
"purchase": {
"id": "d376ca29-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"clinic_patient_id": "b1b0b8de-0000-4000-8000-000000000001",
"pack_id": "6acd6212-0000-4000-8000-000000000001",
"budget_id": null,
"obligation_group_id": "a067dce6-0000-4000-8000-000000000001",
"name_snapshot": "Pack 4 controles de ortodoncia",
"session_count": 4,
"precio_clp": 96000,
"purchased_at": "2026-09-22T22:21:14.782Z",
"expires_at": "2027-04-20",
"frozen_from": "2026-10-01",
"frozen_to": "2026-10-31",
"freeze_count": 1,
"status": "frozen",
"cancelled_reason": null,
"price_source": "catalog",
"price_override_reason": null,
"price_explain": [],
"sold_by": null,
"source": "native",
"external_id": null,
"data": {},
"synced_at": null,
"created_at": "2026-09-22T22:21:14.781Z",
"updated_at": "2026-09-22T22:21:14.845Z"
},
"balance": {
"session_count": 4,
"consumed": 0,
"reversed": 1,
"remaining": 4,
"over_consumed": false,
"frozen": false,
"date_passed": false,
"days_to_expiry": 210,
"effective_status": "active"
},
"today": "2026-09-22",
"ledger": {
"id": "a067dce6-0000-4000-8000-000000000001",
"contact_id": "1a73af9e-0000-4000-8000-000000000001",
"kind": "pack",
"label": "Pack 4 controles de ortodoncia",
"status": "open",
"obligation_count": 1,
"expected_clp": 96000,
"allocated_clp": 0,
"outstanding_clp": 96000,
"state": "pending",
"obligations": [
{
"id": "b44936d0-0000-4000-8000-000000000001",
"contact_id": "1a73af9e-0000-4000-8000-000000000001",
"kind": "pack",
"direction": "receivable",
"label": "Pack 4 controles de ortodoncia",
"expected_clp": 96000,
"allocated_clp": 0,
"outstanding_clp": 96000,
"overpaid_clp": 0,
"state": "pending",
"provenance": null,
"is_overdue": false,
"status": "open",
"disposition": null,
"opened_on": "2026-09-22",
"due_on": null,
"closed_at": null,
"closed_reason": null,
"group_id": "a067dce6-0000-4000-8000-000000000001",
"source": "staff",
"document": null,
"allocations": []
}
],
"created_at": "2026-09-22T22:21:14.760Z"
}
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /clinic/packs/purchases/{id}/sessions
One pack purchase's session ledger
Consumptions AND reversals, oldest first, with the derived balance. A reversal is a ROW (never a delete), which is what makes the balance survive a voided session.
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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
curl https://api.vitrinadev.com/api/v1/clinic/packs/purchases/<id>/sessions \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"purchase": {
"purchase_id": "d376ca29-0000-4000-8000-000000000001",
"name": "Pack 4 controles de ortodoncia",
"pack_id": "6acd6212-0000-4000-8000-000000000001",
"source": "native",
"purchased_at": "2026-09-22T22:21:14.782Z",
"expires_at": "2027-03-21",
"frozen_from": null,
"frozen_to": null,
"freeze_count": 0,
"stored_status": "active",
"precio_clp": 96000,
"obligation_group_id": "a067dce6-0000-4000-8000-000000000001",
"balance": {
"session_count": 4,
"consumed": 1,
"reversed": 0,
"remaining": 3,
"over_consumed": false,
"frozen": false,
"date_passed": false,
"days_to_expiry": 180,
"effective_status": "active"
},
"last_session_at": "2026-09-22T22:21:14.813Z",
"next_session_due_after": null
},
"sessions": [
{
"id": "fcbdc4c2-0000-4000-8000-000000000001",
"consumed_at": "2026-09-22T22:21:14.813Z",
"appointment_id": null,
"clinic_service_id": "4cf5bc59-0000-4000-8000-000000000001",
"professional_id": "18342d1b-0000-4000-8000-000000000001",
"reversal_of": null,
"note": "Control 1 de 4",
"source": "native"
}
]
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /clinic/packs/purchases/{id}/sessions/{sessionId}/reverse
Undo a consumption
INSERTS the row that reverses it (reversal_of); never deletes. A consumption may be reversed at most once — a second reversal would credit the derived balance twice.
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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí | |
sessionId | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
note | string | null | — | máx. 500 |
curl -X POST https://api.vitrinadev.com/api/v1/clinic/packs/purchases/<id>/sessions/<id>/reverse \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"note": "Consumida por error"
}'Ejemplo de respuesta (201)
{
"data": {
"session": {
"id": "fcbdc4c2-0000-4000-8000-000000000002",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"pack_purchase_id": "d376ca29-0000-4000-8000-000000000001",
"appointment_id": null,
"clinic_appointment_service_id": null,
"clinic_service_id": "4cf5bc59-0000-4000-8000-000000000001",
"professional_id": "18342d1b-0000-4000-8000-000000000001",
"consumed_at": "2026-09-22T22:21:14.833Z",
"reversal_of": "fcbdc4c2-0000-4000-8000-000000000001",
"note": "Consumida por error",
"override_reason": null,
"created_by": null,
"source": "native",
"external_id": null,
"data": {},
"synced_at": null,
"created_at": "2026-09-22T22:21:14.832Z"
},
"view": {
"purchase": {
"id": "d376ca29-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"clinic_patient_id": "b1b0b8de-0000-4000-8000-000000000001",
"pack_id": "6acd6212-0000-4000-8000-000000000001",
"budget_id": null,
"obligation_group_id": "a067dce6-0000-4000-8000-000000000001",
"name_snapshot": "Pack 4 controles de ortodoncia",
"session_count": 4,
"precio_clp": 96000,
"purchased_at": "2026-09-22T22:21:14.782Z",
"expires_at": "2027-03-21",
"frozen_from": null,
"frozen_to": null,
"freeze_count": 0,
"status": "active",
"cancelled_reason": null,
"price_source": "catalog",
"price_override_reason": null,
"price_explain": [],
"sold_by": null,
"source": "native",
"external_id": null,
"data": {},
"synced_at": null,
"created_at": "2026-09-22T22:21:14.781Z",
"updated_at": "2026-09-22T22:21:14.784Z"
},
"balance": {
"session_count": 4,
"consumed": 0,
"reversed": 1,
"remaining": 4,
"over_consumed": false,
"frozen": false,
"date_passed": false,
"days_to_expiry": 180,
"effective_status": "active"
},
"today": "2026-09-22",
"ledger": {
"id": "a067dce6-0000-4000-8000-000000000001",
"contact_id": "1a73af9e-0000-4000-8000-000000000001",
"kind": "pack",
"label": "Pack 4 controles de ortodoncia",
"status": "open",
"obligation_count": 1,
"expected_clp": 96000,
"allocated_clp": 0,
"outstanding_clp": 96000,
"state": "pending",
"obligations": [
{
"id": "b44936d0-0000-4000-8000-000000000001",
"contact_id": "1a73af9e-0000-4000-8000-000000000001",
"kind": "pack",
"direction": "receivable",
"label": "Pack 4 controles de ortodoncia",
"expected_clp": 96000,
"allocated_clp": 0,
"outstanding_clp": 96000,
"overpaid_clp": 0,
"state": "pending",
"provenance": null,
"is_overdue": false,
"status": "open",
"disposition": null,
"opened_on": "2026-09-22",
"due_on": null,
"closed_at": null,
"closed_reason": null,
"group_id": "a067dce6-0000-4000-8000-000000000001",
"source": "staff",
"document": null,
"allocations": []
}
],
"created_at": "2026-09-22T22:21:14.760Z"
}
}
}
}Responde: 201 · 400 · 401 · 403 · 404 · 409 · 429
POST /clinic/packs/purchases/{id}/unfreeze
Lift a freeze
Closes an open window at today and pushes expires_at out by the days the pack actually spent frozen.
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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
curl -X POST https://api.vitrinadev.com/api/v1/clinic/packs/purchases/<id>/unfreeze \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"purchase": {
"id": "d376ca29-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"clinic_patient_id": "b1b0b8de-0000-4000-8000-000000000001",
"pack_id": "6acd6212-0000-4000-8000-000000000001",
"budget_id": null,
"obligation_group_id": "a067dce6-0000-4000-8000-000000000001",
"name_snapshot": "Pack 4 controles de ortodoncia",
"session_count": 4,
"precio_clp": 96000,
"purchased_at": "2026-09-22T22:21:14.782Z",
"expires_at": "2027-04-20",
"frozen_from": null,
"frozen_to": null,
"freeze_count": 1,
"status": "active",
"cancelled_reason": null,
"price_source": "catalog",
"price_override_reason": null,
"price_explain": [],
"sold_by": null,
"source": "native",
"external_id": null,
"data": {},
"synced_at": null,
"created_at": "2026-09-22T22:21:14.781Z",
"updated_at": "2026-09-22T22:21:14.858Z"
},
"balance": {
"session_count": 4,
"consumed": 0,
"reversed": 1,
"remaining": 4,
"over_consumed": false,
"frozen": false,
"date_passed": false,
"days_to_expiry": 210,
"effective_status": "active"
},
"today": "2026-09-22",
"ledger": {
"id": "a067dce6-0000-4000-8000-000000000001",
"contact_id": "1a73af9e-0000-4000-8000-000000000001",
"kind": "pack",
"label": "Pack 4 controles de ortodoncia",
"status": "open",
"obligation_count": 1,
"expected_clp": 96000,
"allocated_clp": 0,
"outstanding_clp": 96000,
"state": "pending",
"obligations": [
{
"id": "b44936d0-0000-4000-8000-000000000001",
"contact_id": "1a73af9e-0000-4000-8000-000000000001",
"kind": "pack",
"direction": "receivable",
"label": "Pack 4 controles de ortodoncia",
"expected_clp": 96000,
"allocated_clp": 0,
"outstanding_clp": 96000,
"overpaid_clp": 0,
"state": "pending",
"provenance": null,
"is_overdue": false,
"status": "open",
"disposition": null,
"opened_on": "2026-09-22",
"due_on": null,
"closed_at": null,
"closed_reason": null,
"group_id": "a067dce6-0000-4000-8000-000000000001",
"source": "staff",
"document": null,
"allocations": []
}
],
"created_at": "2026-09-22T22:21:14.760Z"
}
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /clinic/patients/{id}/packs
A patient's pack balances
Remaining sessions and expiry per purchase, DERIVED from the session rows (never a stored counter) against the clinic's own calendar day. A FROZEN pack does not expire while frozen. live_only=true narrows to the purchases that can still be consumed.
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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí | |
live_only | query | true \ | false | no |
curl https://api.vitrinadev.com/api/v1/clinic/patients/<id>/packs \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"patient": {
"id": "b1b0b8de-0000-4000-8000-000000000001",
"source": "native",
"nombre": "María José",
"apellidos": "Fuentes Lagos"
},
"today": "2026-09-22",
"timezone": "America/Santiago",
"packs": [
{
"purchase_id": "d376ca29-0000-4000-8000-000000000001",
"name": "Pack 4 controles de ortodoncia",
"pack_id": "6acd6212-0000-4000-8000-000000000001",
"source": "native",
"purchased_at": "2026-09-22T22:21:14.782Z",
"expires_at": "2027-04-20",
"frozen_from": null,
"frozen_to": null,
"freeze_count": 1,
"stored_status": "active",
"precio_clp": 96000,
"obligation_group_id": "a067dce6-0000-4000-8000-000000000001",
"balance": {
"session_count": 4,
"consumed": 0,
"reversed": 1,
"remaining": 4,
"over_consumed": false,
"frozen": false,
"date_passed": false,
"days_to_expiry": 210,
"effective_status": "active"
},
"last_session_at": "2026-09-22T22:21:14.813Z",
"next_session_due_after": null
}
]
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
Recursos humanos de la clínica
La conexión de solo lectura con Buk y el costo de nómina del mes, sumado a partir de las liquidaciones cerradas sin guardar el detalle por persona.
Registro de pacientes
Quién es paciente de la clínica y quién responde por él: la ficha de identidad, los duplicados y los vínculos de familia. Marcada como sensible — pide su propio permiso y ninguna aplicación conectada la alcanza.