VitrinaAPI

Session packs

Sessions sold up front: the catalogue's pack, one patient's purchase and its derived balance.

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.

List services and their prices explains this resource in prose, with runnable examples.

MethodPathWhat it does
GET/clinic/packsList pack definitions (the catalog)
POST/clinic/packsDefine a pack (the catalog row)
DELETE/clinic/packs/{id}Retire a pack
PATCH/clinic/packs/{id}Edit a pack
POST/clinic/packs/purchasesSell 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}/cancelAnular or refund a pack purchase
POST/clinic/packs/purchases/{id}/consumeConsume one session
POST/clinic/packs/purchases/{id}/freezeFreeze a pack
GET/clinic/packs/purchases/{id}/sessionsOne pack purchase's session ledger
POST/clinic/packs/purchases/{id}/sessions/{sessionId}/reverseUndo a consumption
POST/clinic/packs/purchases/{id}/unfreezeLift a freeze
GET/clinic/patients/{id}/packsA 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.

ParameterInTypeRequiredConstraints
activequerytrue \falseno
pagequeryintegerno≥ 1, por defecto 1
limitqueryintegerno≥ 1, ≤ 200, por defecto 50
curl https://api.vitrinadev.com/api/v1/clinic/packs \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (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
  }
}

Answers: 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.

Body

FieldTypeRequiredConstraints
namestringyesmín. 1, máx. 200
descriptionstring | null—máx. 2000
session_countintegeryes≥ 1, ≤ 999
precio_clpinteger | null—≥ 0, ≤ 999999999999
validity_daysinteger | null—≥ 1, ≤ 3650
cadence_daysinteger | null—≥ 1, ≤ 365
freeze_allowedboolean—
freeze_max_countinteger—≥ 0, ≤ 10
transferableboolean—
per_session_forfeits_priceboolean—
requires_full_prepaymentboolean—
online_bookableboolean—
activeboolean—
itemsobject[]—
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
      }
    ]
  }'

Example response (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"
      }
    ]
  }
}

Answers: 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.

ParameterInTypeRequiredConstraints
idpathuuidyes
curl -X DELETE https://api.vitrinadev.com/api/v1/clinic/packs/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (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": []
  }
}

Answers: 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.

ParameterInTypeRequiredConstraints
idpathuuidyes

Body

FieldTypeRequiredConstraints
namestring—mín. 1, máx. 200
descriptionstring | null—máx. 2000
session_countinteger—≥ 1, ≤ 999
precio_clpinteger | null—≥ 0, ≤ 999999999999
validity_daysinteger | null—≥ 1, ≤ 3650
cadence_daysinteger | null—≥ 1, ≤ 365
freeze_allowedboolean—
freeze_max_countinteger—≥ 0, ≤ 10
transferableboolean—
per_session_forfeits_priceboolean—
requires_full_prepaymentboolean—
online_bookableboolean—
activeboolean—
itemsobject[]—
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"
  }'

Example response (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"
      }
    ]
  }
}

Answers: 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.

Body

FieldTypeRequiredConstraints
clinic_patient_idstring | null—
contact_idstring | null—
pack_idstring | null—
session_countinteger | null—≥ 1, ≤ 999
namestring | null—mín. 1, máx. 200
precio_clpinteger | null—≥ 0, ≤ 999999999999
price_override_reasonstring | null—mín. 3, máx. 500
use_quoteboolean—
expires_atstring | null—patrón ^\d{4}-\d{2}-\d{2}$
due_onstring | 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"
  }'

Example response (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"
    }
  }
}

Answers: 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.

ParameterInTypeRequiredConstraints
idpathuuidyes
curl https://api.vitrinadev.com/api/v1/clinic/packs/purchases/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (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"
    }
  }
}

Answers: 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.

ParameterInTypeRequiredConstraints
idpathuuidyes

Body

FieldTypeRequiredConstraints
reasonstringyesmín. 3, máx. 500
refundedboolean—
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
  }'

Example response (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
  }
}

Answers: 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.

ParameterInTypeRequiredConstraints
idpathuuidyes

Body

FieldTypeRequiredConstraints
appointment_idstring | null—
clinic_appointment_service_idstring | null—
clinic_service_idstring | null—
professional_idstring | null—
consumed_atstring | null—date-time
notestring | null—máx. 500
override_reasonstring | 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"
  }'

Example response (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"
      }
    }
  }
}

Answers: 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.

ParameterInTypeRequiredConstraints
idpathuuidyes

Body

FieldTypeRequiredConstraints
fromstring | null—patrón ^\d{4}-\d{2}-\d{2}$
tostring | null—patrón ^\d{4}-\d{2}-\d{2}$
reasonstring | 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"
  }'

Example response (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"
    }
  }
}

Answers: 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.

ParameterInTypeRequiredConstraints
idpathuuidyes
curl https://api.vitrinadev.com/api/v1/clinic/packs/purchases/<id>/sessions \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (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"
      }
    ]
  }
}

Answers: 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.

ParameterInTypeRequiredConstraints
idpathuuidyes
sessionIdpathuuidyes

Body

FieldTypeRequiredConstraints
notestring | 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"
  }'

Example response (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"
      }
    }
  }
}

Answers: 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.

ParameterInTypeRequiredConstraints
idpathuuidyes
curl -X POST https://api.vitrinadev.com/api/v1/clinic/packs/purchases/<id>/unfreeze \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (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"
    }
  }
}

Answers: 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.

ParameterInTypeRequiredConstraints
idpathuuidyes
live_onlyquerytrue \falseno
curl https://api.vitrinadev.com/api/v1/clinic/patients/<id>/packs \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (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
      }
    ]
  }
}

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

On this page