VitrinaAPI

Patient register

Who is a patient of the clinic and who answers for them: the identity record, the duplicates and the family bonds. Marked sensitive — it asks for its own scope and no connected app reaches it.

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.

Find and merge patient records explains this resource in prose, with runnable examples.

MethodPathWhat it does
POST/clinic/auto-links/{id}/reject«No es esta persona» — reject an automatic link for good
GET/clinic/contacts/{contactId}/auto-linksPatients automatically linked to a contact (Ads identity only)
GET/clinic/contacts/{contactId}/patientThe contact page's Paciente card
GET/clinic/contacts/{contactId}/paymentsA contact's payment history
GET/clinic/contacts/{contactId}/payments/{paymentId}/boletaA payment's boleta (PDF)
GET/clinic/contacts/{contactId}/relationshipsEvery patient one contact acts for
GET/clinic/patientsList clinic patients
POST/clinic/patientsCreate a native patient
DELETE/clinic/patients/{id}Delete a native patient
GET/clinic/patients/{id}Get a clinic patient
PATCH/clinic/patients/{id}Update a patient
GET/clinic/patients/{id}/attendanceA patient's attendance history
GET/clinic/patients/{id}/auto-linksAutomatic contact links of a patient (Ads identity only)
GET/clinic/patients/{id}/citasA patient's mirrored citas, newest first
POST/clinic/patients/{id}/mergeMerge a patient into another (tombstone, never a delete)
GET/clinic/patients/{id}/relationshipsThe family of one patient (apoderados, cargas, pareja)
POST/clinic/patients/{id}/relationshipsAdd somebody to a patient’s family
GET/clinic/patients/duplicatesPossible duplicate patients, grouped with the reason
DELETE/clinic/relationships/{id}Remove a family bond
PATCH/clinic/relationships/{id}Edit one family bond

POST /clinic/auto-links/{id}/reject

«No es esta persona» — reject an automatic link for good

Marks the pair rejected: no later matching run re-opens it, and the Ads outcomes sent under it that have not left Vitrina yet are withheld (withheld_outcomes says how many). What already reached the ad platforms is not recalled. Idempotent. Answers the row. 404 when the id is not this clinic’s.

Connected apps: refused with 403 CONNECTED_APP_SENSITIVE_DATA, whatever scopes they hold — this operation carries a dato sensible (ADR 0106 §4). Only an API key or a personal token of the workspace that holds the scope reaches it.

ParameterInTypeRequiredConstraints
idpathuuidyes

Body

FieldTypeRequiredConstraints
curl -X POST https://api.vitrinadev.com/api/v1/clinic/auto-links/<id>/reject \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

Example response (200)

{
  "data": {
    "id": "a7a7a7a7-0000-4000-8000-000000000001",
    "clinic_patient_id": "b1b0b8de-0000-4000-8000-000000000001",
    "contact_id": "1a73af9e-0000-4000-8000-000000000001",
    "patient_name": "María José Fuentes Lagos",
    "contact_name": "María José Fuentes Lagos",
    "status": "rejected",
    "method": "phone_unique",
    "matched_at": "2026-09-27T14:10:02.118Z",
    "run_id": "a8a8a8a8-0000-4000-8000-000000000001",
    "revoked_at": "2026-09-28T15:31:09.004Z",
    "revoked_reason": "rejected",
    "sent_count": 3,
    "last_sent_at": "2026-09-28T12:02:40.551Z",
    "withheld_outcomes": 1
  }
}

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

Patients automatically linked to a contact (Ads identity only)

The contact side of the same automatic links — what the contact ficha shows as «Vinculado automáticamente». An empty list, not a 404, when there is none. Each row: id, clinic_patient_id, contact_id, patient_name, contact_name, status (live | revoked — a run found it no longer unique or ineligible — | rejected — a person said it is not this person, never re-opened), method (phone_unique | email_unique: which identifier was held by exactly one patient and one contact; chat_rut: the patient's RUT typed by exactly one contact in its own ad chat, corroborated by the typed email or by the ficha appearing after it — the value itself is never stored), matched_at, run_id (the matching run, provenance), revoked_at, revoked_reason, sent_count / last_sent_at (Ads outcomes sent under the pair). Live first.

Connected apps: refused with 403 CONNECTED_APP_SENSITIVE_DATA, whatever scopes they hold — this operation carries a dato sensible (ADR 0106 §4). Only an API key or a personal token of the workspace that holds the scope reaches it.

ParameterInTypeRequiredConstraints
contactIdpathuuidyes
curl https://api.vitrinadev.com/api/v1/clinic/contacts/<id>/auto-links \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "data": [
      {
        "id": "a7a7a7a7-0000-4000-8000-000000000001",
        "clinic_patient_id": "b1b0b8de-0000-4000-8000-000000000001",
        "contact_id": "1a73af9e-0000-4000-8000-000000000001",
        "patient_name": "María José Fuentes Lagos",
        "contact_name": "María José Fuentes Lagos",
        "status": "live",
        "method": "phone_unique",
        "matched_at": "2026-09-27T14:10:02.118Z",
        "run_id": "a8a8a8a8-0000-4000-8000-000000000001",
        "revoked_at": null,
        "revoked_reason": null,
        "sent_count": 3,
        "last_sent_at": "2026-09-28T12:02:40.551Z"
      }
    ]
  }
}

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

GET /clinic/contacts/{contactId}/patient

The contact page's Paciente card

The linked patient plus its next and last cita. 404 not_linked when the contact is not a patient.

Connected apps: refused with 403 CONNECTED_APP_SENSITIVE_DATA, whatever scopes they hold — this operation carries a dato sensible (ADR 0106 §4). Only an API key or a personal token of the workspace that holds the scope reaches it.

ParameterInTypeRequiredConstraints
contactIdpathuuidyes
curl https://api.vitrinadev.com/api/v1/clinic/contacts/<id>/patient \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "patient": {
      "id": "b1b0b8de-0000-4000-8000-000000000001",
      "source": "native",
      "external_id": null,
      "rut": "11.111.111-1",
      "nombre": "María José",
      "apellidos": "Fuentes Lagos",
      "email": "[email protected]",
      "phone": "+56987654321",
      "birthdate": "1990-04-12",
      "nombre_social": null,
      "prevision": "Isapre Consalud",
      "sexo": "F",
      "numero_ficha": "44110",
      "ciudad": null,
      "region": null,
      "comuna": "Maipú",
      "direccion": null,
      "description": null,
      "enabled": true,
      "synced_at": null,
      "contact_id": "1a73af9e-0000-4000-8000-000000000001",
      "created_at": "2026-09-22T22:21:14.311Z",
      "updated_at": "2026-09-22T22:21:17.194Z"
    },
    "next_cita": {
      "id": "d93e8eec-0000-4000-8000-000000000001",
      "display_id": "A-1",
      "engine": "native",
      "external_id": null,
      "starts_at": "2026-10-01T13:00:00.000Z",
      "ends_at": "2026-10-01T13:30:00.000Z",
      "status": "confirmed",
      "estado_nombre": null,
      "profesional": "Ana Rojas Vidal",
      "tratamiento": null,
      "sucursal": "Sucursal Maipú",
      "sillon": null,
      "motivo": null,
      "comentarios": null
    },
    "last_cita": null,
    "attendance": {
      "completed": 0,
      "no_show": 0,
      "cancelled": 0
    },
    "recent_citas": [
      {
        "id": "d93e8eec-0000-4000-8000-000000000001",
        "display_id": "A-1",
        "engine": "native",
        "external_id": null,
        "starts_at": "2026-10-01T13:00:00.000Z",
        "ends_at": "2026-10-01T13:30:00.000Z",
        "status": "confirmed",
        "estado_nombre": null,
        "profesional": "Ana Rojas Vidal",
        "tratamiento": null,
        "sucursal": "Sucursal Maipú",
        "sillon": null,
        "motivo": null,
        "comentarios": null
      }
    ]
  }
}

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

GET /clinic/contacts/{contactId}/payments

A contact's payment history

The pagos of every patient this contact speaks for, from Vitrina’s own data only (never a live vendor read): the Dentalink / Medilink / Reservo mirror’s rows on the days the vendor owns the money, the native ledger’s on every other day — one peso is never counted twice. Newest first; summary covers every row, not just the page. mirror_missing = a connected vendor clinic whose money is not mirrored (the history is incomplete). 404 for a contact outside the tenant. clinic_money:read.

Connected apps: refused with 403 CONNECTED_APP_SENSITIVE_DATA, whatever scopes they hold — this operation carries a dato sensible (ADR 0106 §4). Only an API key or a personal token of the workspace that holds the scope reaches it.

ParameterInTypeRequiredConstraints
contactIdpathuuidyes
limitqueryintegerno≥ 1, ≤ 200, por defecto 50
curl https://api.vitrinadev.com/api/v1/clinic/contacts/<id>/payments \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "contact_id": "0199c4b6-778a-7a98-a427-8a9fea5ec5dd",
    "source": {
      "vendor": "dentalink",
      "mirrored": true,
      "mirror_enabled": true,
      "mirror_from": "2025-10-01",
      "last_synced_at": "2026-10-07T11:00:00.000Z"
    },
    "mirror_missing": false,
    "patients": [
      {
        "id": "0199c4b6-77a2-752e-bbac-55e62917b0e0",
        "name": "Camila Rojas Fuentes",
        "relationship": "titular"
      }
    ],
    "summary": {
      "total_paid_clp": 105000,
      "payment_count": 2,
      "last_paid_on": "2026-10-06",
      "outstanding_clp": 180000
    },
    "payments": [
      {
        "id": "0199c4b6-7800-7000-8000-000000000001",
        "kind": "payment",
        "origin": "dentalink",
        "paid_on": "2026-10-06",
        "amount_clp": 60000,
        "method": "Tarjeta de débito",
        "instrument": null,
        "folio": "11877",
        "voided": false,
        "patient_id": "0199c4b6-77a2-752e-bbac-55e62917b0e0",
        "patient_name": null,
        "concept": "Control de ortodoncia",
        "professional": "Dra. Paula Soto",
        "appointment_starts_at": "2026-10-06T13:00:00.000Z",
        "boleta": "available"
      },
      {
        "id": "0199c4b6-7800-7000-8000-000000000002",
        "kind": "refund",
        "origin": "dentalink",
        "paid_on": "2026-04-30",
        "amount_clp": 15000,
        "method": null,
        "instrument": null,
        "folio": null,
        "voided": false,
        "patient_id": "0199c4b6-77a2-752e-bbac-55e62917b0e0",
        "patient_name": null,
        "concept": null,
        "professional": null,
        "appointment_starts_at": null,
        "boleta": "none"
      }
    ],
    "has_more": false
  }
}

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

GET /clinic/contacts/{contactId}/payments/{paymentId}/boleta

A payment's boleta (PDF)

The boleta PDF of one row of GET /clinic/contacts/\{contactId\}/payments (the payment must be one that read returns for this contact; 404 otherwise). Native: the linked tax document. Dentalink / Medilink: resolved from the practice system on the first call and remembered (only the document id — never its link). Reservo: the venta’s boleta. The bytes are proxied; no vendor URL ever leaves the server. Errors: 404 boleta_not_found (the practice system has no boleta for it), 502 boleta_pdf_unavailable, 409 boleta_not_supported, 503 reservo_session, 401 healthatom_permission. clinic_money:read.

Connected apps: refused with 403 CONNECTED_APP_SENSITIVE_DATA, whatever scopes they hold — this operation carries a dato sensible (ADR 0106 §4). Only an API key or a personal token of the workspace that holds the scope reaches it.

ParameterInTypeRequiredConstraints
contactIdpathuuidyes
paymentIdpathuuidyes
curl https://api.vitrinadev.com/api/v1/clinic/contacts/<id>/payments/<id>/boleta \
  -H "Authorization: Bearer $VITRINA_KEY"

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

GET /clinic/contacts/{contactId}/relationships

Every patient one contact acts for

The conversation side of the same table, and what the contact ficha’s «Familia y apoderados» block renders. A contact with no clinical bonds answers an empty list, not a 404.

Connected apps: refused with 403 CONNECTED_APP_SENSITIVE_DATA, whatever scopes they hold — this operation carries a dato sensible (ADR 0106 §4). Only an API key or a personal token of the workspace that holds the scope reaches it.

ParameterInTypeRequiredConstraints
contactIdpathuuidyes
curl https://api.vitrinadev.com/api/v1/clinic/contacts/<id>/relationships \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "data": [
      {
        "id": "6a73a912-0000-4000-8000-000000000001",
        "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
        "clinic_patient_id": "b1b0b8de-0000-4000-8000-000000000001",
        "contact_id": "1a73af9e-0000-4000-8000-000000000001",
        "related_patient_id": null,
        "role": "titular",
        "is_primary": true,
        "can_book": true,
        "can_read_record": false,
        "verified_method": "phone_match",
        "verified_at": "2026-09-22T22:21:14.390Z",
        "expires_at": null,
        "note": null,
        "created_by": null,
        "created_at": "2026-09-22T22:21:14.389Z",
        "updated_at": "2026-09-22T22:21:14.389Z",
        "contact_name": "María José Fuentes Lagos",
        "contact_phone": "+56987654321",
        "patient_name": "María José Fuentes Lagos",
        "expired": false
      }
    ]
  }
}

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

GET /clinic/patients

List clinic patients

search (min 2 chars) matches the name accent-insensitively, a normalized-RUT prefix, or a phone-digits substring. Rows carry contact_id — the linked CRM contact, or null.

Connected apps: refused with 403 CONNECTED_APP_SENSITIVE_DATA, whatever scopes they hold — this operation carries a dato sensible (ADR 0106 §4). Only an API key or a personal token of the workspace that holds the scope reaches it.

ParameterInTypeRequiredConstraints
searchquerystringnomín. 2, máx. 120
sortquerynombre \rut \phone \
dirqueryasc \descno
pagequeryintegerno≥ 1, por defecto 1
limitqueryintegerno≥ 1, ≤ 200, por defecto 50
curl https://api.vitrinadev.com/api/v1/clinic/patients \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "data": [
      {
        "id": "b1b0b8de-0000-4000-8000-000000000001",
        "source": "native",
        "external_id": null,
        "rut": "11.111.111-1",
        "nombre": "María José",
        "apellidos": "Fuentes Lagos",
        "email": "[email protected]",
        "phone": "+56987654321",
        "birthdate": "1990-04-12",
        "nombre_social": null,
        "prevision": "Fonasa",
        "sexo": "F",
        "numero_ficha": null,
        "ciudad": null,
        "region": null,
        "comuna": "Maipú",
        "direccion": null,
        "description": null,
        "enabled": true,
        "synced_at": null,
        "contact_id": null,
        "created_at": "2026-09-22T22:21:14.311Z",
        "updated_at": "2026-09-22T22:21:14.311Z"
      }
    ],
    "total": 1,
    "page": 1,
    "limit": 20
  }
}

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

POST /clinic/patients

Create a native patient

Create one of the clinic's OWN (native) patients — source is 'native'. 409 not_native_source when the tenant's clinic source is not 'native'. At least one of nombre/apellidos/rut is required; birthdate is a YYYY-MM-DD calendar day.

Connected apps: refused with 403 CONNECTED_APP_SENSITIVE_DATA, whatever scopes they hold — this operation carries a dato sensible (ADR 0106 §4). Only an API key or a personal token of the workspace that holds the scope reaches it.

Body

FieldTypeRequiredConstraints
nombrestring | null—máx. 160
apellidosstring | null—máx. 160
rutstring | null—máx. 20
emailstring | null—máx. 160
phonestring | null—máx. 40
birthdatestring | null—patrón ^\d{4}-\d{2}-\d{2}$
nombre_socialstring | null—máx. 160
previsionstring | null—máx. 120
sexostring | null—máx. 40
numero_fichastring | null—máx. 60
ciudadstring | null—máx. 120
regionstring | null—máx. 120
comunastring | null—máx. 120
direccionstring | null—máx. 240
descriptionstring | null—máx. 2000
curl -X POST https://api.vitrinadev.com/api/v1/clinic/patients \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "nombre": "María José",
    "apellidos": "Fuentes Lagos",
    "rut": "11.111.111-1",
    "email": "[email protected]",
    "phone": "+56987654321",
    "birthdate": "1990-04-12",
    "prevision": "Fonasa",
    "sexo": "F",
    "comuna": "Maipú"
  }'

Example response (201)

{
  "data": {
    "id": "b1b0b8de-0000-4000-8000-000000000001",
    "source": "native",
    "external_id": null,
    "rut": "11.111.111-1",
    "nombre": "María José",
    "apellidos": "Fuentes Lagos",
    "email": "[email protected]",
    "phone": "+56987654321",
    "birthdate": "1990-04-12",
    "nombre_social": null,
    "prevision": "Fonasa",
    "sexo": "F",
    "numero_ficha": null,
    "ciudad": null,
    "region": null,
    "comuna": "Maipú",
    "direccion": null,
    "description": null,
    "enabled": true,
    "synced_at": null,
    "contact_id": null,
    "created_at": "2026-09-22T22:21:14.311Z",
    "updated_at": "2026-09-22T22:21:14.311Z"
  }
}

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

DELETE /clinic/patients/{id}

Delete a native patient

Native → hard-deleted (its contact link survives with a null clinic_patient_id). Synced (vendor) → 400 cannot_delete_synced.

Connected apps: refused with 403 CONNECTED_APP_SENSITIVE_DATA, whatever scopes they hold — this operation carries a dato sensible (ADR 0106 §4). Only an API key or a personal token of the workspace that holds the scope reaches it.

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

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

GET /clinic/patients/{id}

Get a clinic patient

Also carries origin: the patient's Origen, Meta (with the ad, ad set and campaign) Orgánico (a linked contact with no ad touch) or Sin dato (unknown: no linked contact) — the attribution Vitrina Ads reports, through the patient's linked contact. link is the contact link (contact_id, verified_method, verified_at) plus revoked_at / revoked_reason / revoked_method: set while the automatic phone-link guard holds the link revoked (a minor, a number shared by several fichas, …) — it still books but no longer reveals the record, and revoked_method is what the link was before.

Connected apps: refused with 403 CONNECTED_APP_SENSITIVE_DATA, whatever scopes they hold — this operation carries a dato sensible (ADR 0106 §4). Only an API key or a personal token of the workspace that holds the scope reaches it.

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

Example response (200)

{
  "data": {
    "id": "b1b0b8de-0000-4000-8000-000000000001",
    "source": "native",
    "external_id": null,
    "rut": "11.111.111-1",
    "nombre": "María José",
    "apellidos": "Fuentes Lagos",
    "email": "[email protected]",
    "phone": "+56987654321",
    "birthdate": "1990-04-12",
    "nombre_social": null,
    "prevision": "Fonasa",
    "sexo": "F",
    "numero_ficha": null,
    "ciudad": null,
    "region": null,
    "comuna": "Maipú",
    "direccion": null,
    "description": null,
    "enabled": true,
    "synced_at": null,
    "contact_id": null,
    "created_at": "2026-09-22T22:21:14.311Z",
    "updated_at": "2026-09-22T22:21:14.311Z",
    "data": {},
    "link": null
  }
}

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

PATCH /clinic/patients/{id}

Update a patient

Edit a native patient. 400 not_editable on a synced (non-native) row — those are managed in the clinic system, not Vitrina.

Connected apps: refused with 403 CONNECTED_APP_SENSITIVE_DATA, whatever scopes they hold — this operation carries a dato sensible (ADR 0106 §4). Only an API key or a personal token of the workspace that holds the scope reaches it.

ParameterInTypeRequiredConstraints
idpathuuidyes

Body

FieldTypeRequiredConstraints
nombrestring | null—máx. 160
apellidosstring | null—máx. 160
rutstring | null—máx. 20
emailstring | null—máx. 160
phonestring | null—máx. 40
birthdatestring | null—patrón ^\d{4}-\d{2}-\d{2}$
nombre_socialstring | null—máx. 160
previsionstring | null—máx. 120
sexostring | null—máx. 40
numero_fichastring | null—máx. 60
ciudadstring | null—máx. 120
regionstring | null—máx. 120
comunastring | null—máx. 120
direccionstring | null—máx. 240
descriptionstring | null—máx. 2000
curl -X PATCH https://api.vitrinadev.com/api/v1/clinic/patients/<id> \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prevision": "Isapre Consalud",
    "comuna": "Maipú",
    "numero_ficha": "44110"
  }'

Example response (200)

{
  "data": {
    "id": "b1b0b8de-0000-4000-8000-000000000001",
    "source": "native",
    "external_id": null,
    "rut": "11.111.111-1",
    "nombre": "María José",
    "apellidos": "Fuentes Lagos",
    "email": "[email protected]",
    "phone": "+56987654321",
    "birthdate": "1990-04-12",
    "nombre_social": null,
    "prevision": "Isapre Consalud",
    "sexo": "F",
    "numero_ficha": "44110",
    "ciudad": null,
    "region": null,
    "comuna": "Maipú",
    "direccion": null,
    "description": null,
    "enabled": true,
    "synced_at": null,
    "contact_id": null,
    "created_at": "2026-09-22T22:21:14.311Z",
    "updated_at": "2026-09-22T22:21:17.194Z"
  }
}

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

GET /clinic/patients/{id}/attendance

A patient's attendance history

Attended / no-show / late-cancel / cancelled counts, the recent list, and the date of the last late notice — computed from the mirrored citas of ANY engine plus the CURRENT bucket of each cita's clinic_appointment_status. The late-cancel split uses the tenant's window (settings.clinic_attendance.late_cancel_hours, default 8); late_cancel_hours re-counts the same history against another one without changing any setting. late_cancel is a SUBSET of cancelled — never add the two. A past cita nobody closed is counted as unknown, never as attended. This endpoint has no opinion about fees.

Connected apps: refused with 403 CONNECTED_APP_SENSITIVE_DATA, whatever scopes they hold — this operation carries a dato sensible (ADR 0106 §4). Only an API key or a personal token of the workspace that holds the scope reaches it.

ParameterInTypeRequiredConstraints
idpathuuidyes
late_cancel_hoursqueryintegerno≥ 1, ≤ 168
limitqueryintegerno≥ 1, ≤ 50, por defecto 10
curl https://api.vitrinadev.com/api/v1/clinic/patients/<id>/attendance \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "counts": {
      "attended": 0,
      "no_show": 0,
      "late_cancel": 0,
      "cancelled": 0,
      "cancelled_notice_unknown": 0,
      "unknown": 0
    },
    "closed_total": 0,
    "no_show_rate": null,
    "last_late_cancel_at": null,
    "last_no_show_at": null,
    "late_cancel_hours": 8,
    "notice_basis": "cancellation_recorded",
    "recent": [],
    "truncated": false,
    "patient": {
      "id": "b1b0b8de-0000-4000-8000-000000000001",
      "source": "native",
      "nombre": "María José",
      "apellidos": "Fuentes Lagos"
    },
    "matched_by": [
      "contact",
      "ficha"
    ]
  }
}

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

Automatic contact links of a patient (Ads identity only)

The contacts Vitrina matched to this ficha AUTOMATICALLY for Ads outcomes: an E.164 phone or an exact email held by exactly one patient and exactly one contact of the clinic, the patient an adult with a plausible birth date and not a carga, and no family bond or booking link on either side. It is identity for Ads only — it grants no booking and no access to the clinical record, and a family bond or a booking link always outranks it. Each row: id, clinic_patient_id, contact_id, patient_name, contact_name, status (live | revoked — a run found it no longer unique or ineligible — | rejected — a person said it is not this person, never re-opened), method (phone_unique | email_unique: which identifier was held by exactly one patient and one contact; chat_rut: the patient's RUT typed by exactly one contact in its own ad chat, corroborated by the typed email or by the ficha appearing after it — the value itself is never stored), matched_at, run_id (the matching run, provenance), revoked_at, revoked_reason, sent_count / last_sent_at (Ads outcomes sent under the pair). Live first.

Connected apps: refused with 403 CONNECTED_APP_SENSITIVE_DATA, whatever scopes they hold — this operation carries a dato sensible (ADR 0106 §4). Only an API key or a personal token of the workspace that holds the scope reaches it.

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

Example response (200)

{
  "data": {
    "data": [
      {
        "id": "a7a7a7a7-0000-4000-8000-000000000001",
        "clinic_patient_id": "b1b0b8de-0000-4000-8000-000000000001",
        "contact_id": "1a73af9e-0000-4000-8000-000000000001",
        "patient_name": "María José Fuentes Lagos",
        "contact_name": "María José Fuentes Lagos",
        "status": "live",
        "method": "phone_unique",
        "matched_at": "2026-09-27T14:10:02.118Z",
        "run_id": "a8a8a8a8-0000-4000-8000-000000000001",
        "revoked_at": null,
        "revoked_reason": null,
        "sent_count": 3,
        "last_sent_at": "2026-09-28T12:02:40.551Z"
      }
    ]
  }
}

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

GET /clinic/patients/{id}/citas

A patient's mirrored citas, newest first

A patient's mirrored citas, newest first

Connected apps: refused with 403 CONNECTED_APP_SENSITIVE_DATA, whatever scopes they hold — this operation carries a dato sensible (ADR 0106 §4). Only an API key or a personal token of the workspace that holds the scope reaches it.

ParameterInTypeRequiredConstraints
idpathuuidyes
pagequeryintegerno≥ 1, por defecto 1
limitqueryintegerno≥ 1, ≤ 200, por defecto 50
curl https://api.vitrinadev.com/api/v1/clinic/patients/<id>/citas \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "data": [
      {
        "id": "d93e8eec-0000-4000-8000-000000000001",
        "display_id": "A-1",
        "engine": "native",
        "external_id": null,
        "starts_at": "2026-10-01T13:00:00.000Z",
        "ends_at": "2026-10-01T13:30:00.000Z",
        "status": "confirmed",
        "estado_nombre": null,
        "profesional": "Ana Rojas Vidal",
        "tratamiento": null,
        "sucursal": "Sucursal Maipú",
        "sillon": null,
        "motivo": null,
        "comentarios": null
      }
    ],
    "total": 1,
    "page": 1,
    "limit": 50
  }
}

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

POST /clinic/patients/{id}/merge

Merge a patient into another (tombstone, never a delete)

\{id\} is the ficha that STOPS being used; into_patient_id is the one that survives. Repoints the links, the waitlist entries and the family bonds, fills the survivor's BLANK fields from the loser (never overwrites), writes a clinic_patient_merge tombstone with a snapshot of the losing ficha plus who/when/why, and marks the loser merged_into — which keeps its row and its id resolvable (Ley 21.719 + the 15-year retention floor, §H.3). REFUSES 409 vendor_identity_conflict when the loser carries a Medilink/Reservo id the survivor does not share: the next poll would re-create it, so that merge belongs in the vendor. Also 409 for an already-merged ficha on either side.

Connected apps: refused with 403 CONNECTED_APP_SENSITIVE_DATA, whatever scopes they hold — this operation carries a dato sensible (ADR 0106 §4). Only an API key or a personal token of the workspace that holds the scope reaches it.

ParameterInTypeRequiredConstraints
idpathuuidyes

Body

FieldTypeRequiredConstraints
into_patient_iduuidyes
reasonstring—máx. 500
curl -X POST https://api.vitrinadev.com/api/v1/clinic/patients/<id>/merge \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "into_patient_id": "42db9ec9-0000-4000-8000-000000000001",
    "reason": "Misma persona, cargada dos veces"
  }'

Example response (200)

{
  "data": {
    "merge_id": "14091a9f-0000-4000-8000-000000000001",
    "survivor": {
      "id": "42db9ec9-0000-4000-8000-000000000001",
      "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
      "source": "native",
      "external_id": null,
      "rut": "22.222.222-2",
      "nombre": "Tomás",
      "apellidos": "Riquelme Paz",
      "email": null,
      "phone": "+56911112222",
      "birthdate": null,
      "nombre_social": null,
      "prevision": "Isapre",
      "sexo": null,
      "numero_ficha": null,
      "ciudad": null,
      "region": null,
      "comuna": null,
      "direccion": null,
      "description": null,
      "enabled": true,
      "data": {},
      "synced_at": null,
      "merged_into": null,
      "merged_at": null,
      "created_at": "2026-09-22T22:21:14.418Z",
      "updated_at": "2026-09-22T22:21:14.418Z"
    },
    "loser_id": "c6bc29c3-0000-4000-8000-000000000001",
    "moved": {
      "healthatom_patient_link": 0,
      "waitlist_entry": 0,
      "clinic_patient_relationship": 0,
      "clinic_patient_relationship.related_patient_id": 0,
      "clinic_pack_purchase": 0,
      "clinic_encounter": 0,
      "clinic_encounter_note": 0,
      "clinic_history_item": 0,
      "clinic_clinical_flag": 0,
      "clinic_form_response": 0,
      "clinic_consent": 0,
      "clinic_record_photo": 0,
      "clinic_document": 0,
      "clinic_patient_agreement": 0,
      "clinic_appointment_series": 0,
      "clinic_specialty_chart": 0,
      "clinic_budget": 0,
      "clinic_treatment_plan": 0,
      "clinic_tax_document": 0
    },
    "left_on_tombstone": {
      "clinic_patient_relationship": 0,
      "clinic_patient_agreement": 0,
      "clinic_specialty_chart": 0
    },
    "filled_fields": []
  }
}

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

GET /clinic/patients/{id}/relationships

The family of one patient (apoderados, cargas, pareja)

Every contact bonded to this ficha, primary first. Each bond carries two SEPARATE authorities: can_book (may act on the agenda) and can_read_record (⚠ the PHI gate — may be told what is in the clinical record). expired: true marks a bond whose expires_at has passed; every gate already treats it as no bond at all.

Connected apps: refused with 403 CONNECTED_APP_SENSITIVE_DATA, whatever scopes they hold — this operation carries a dato sensible (ADR 0106 §4). Only an API key or a personal token of the workspace that holds the scope reaches it.

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

Example response (200)

{
  "data": {
    "data": [
      {
        "id": "6a73a912-0000-4000-8000-000000000001",
        "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
        "clinic_patient_id": "b1b0b8de-0000-4000-8000-000000000001",
        "contact_id": "1a73af9e-0000-4000-8000-000000000001",
        "related_patient_id": null,
        "role": "titular",
        "is_primary": true,
        "can_book": true,
        "can_read_record": false,
        "verified_method": "phone_match",
        "verified_at": "2026-09-22T22:21:14.390Z",
        "expires_at": null,
        "note": null,
        "created_by": null,
        "created_at": "2026-09-22T22:21:14.389Z",
        "updated_at": "2026-09-22T22:21:14.389Z",
        "contact_name": "María José Fuentes Lagos",
        "contact_phone": "+56987654321",
        "patient_name": "María José Fuentes Lagos",
        "expired": false
      }
    ]
  }
}

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

POST /clinic/patients/{id}/relationships

Add somebody to a patient’s family

Creates one bond between this ficha and a contact. 409 relationship_exists when the pair is already recorded (edit it instead — quietly resetting a PHI flag is not what the caller asked for). Granting can_read_record: true additionally requires the clinic_record:write scope; everything else about a family is clinic administration and needs only clinic:write.

Connected apps: refused with 403 CONNECTED_APP_SENSITIVE_DATA, whatever scopes they hold — this operation carries a dato sensible (ADR 0106 §4). Only an API key or a personal token of the workspace that holds the scope reaches it.

ParameterInTypeRequiredConstraints
idpathuuidyes

Body

FieldTypeRequiredConstraints
contact_iduuidyes
related_patient_idstring | null—
roletitular \apoderado \tutor \
is_primaryboolean—
can_bookboolean—
can_read_recordboolean—
verified_methodphone_match \id_challenge \manual \
expires_atstring | null—date-time
notestring | null—máx. 500
curl -X POST https://api.vitrinadev.com/api/v1/clinic/patients/<id>/relationships \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contact_id": "1a73af9e-0000-4000-8000-000000000001",
    "role": "titular",
    "is_primary": true,
    "can_book": true,
    "verified_method": "phone_match"
  }'

Example response (201)

{
  "data": {
    "id": "6a73a912-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "clinic_patient_id": "b1b0b8de-0000-4000-8000-000000000001",
    "contact_id": "1a73af9e-0000-4000-8000-000000000001",
    "related_patient_id": null,
    "role": "titular",
    "is_primary": true,
    "can_book": true,
    "can_read_record": false,
    "verified_method": "phone_match",
    "verified_at": "2026-09-22T22:21:14.390Z",
    "expires_at": null,
    "note": null,
    "created_by": null,
    "created_at": "2026-09-22T22:21:14.389Z",
    "updated_at": "2026-09-22T22:21:14.389Z",
    "contact_name": "María José Fuentes Lagos",
    "contact_phone": "+56987654321",
    "patient_name": "María José Fuentes Lagos",
    "expired": false
  }
}

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

GET /clinic/patients/duplicates

Possible duplicate patients, grouped with the reason

Scans the LIVE roster (tombstones excluded) for three patterns: the same document, the same normalised phone, and the same name + date of birth. A RUT is canonicalised through the one legal-identity normaliser and a passport is never folded through it, so a mistyped RUT groups with nothing rather than colliding with a real passport. One group per candidate SET (a pair matching on two rules is one card listing both reasons), strongest reason first. vendor_blocked: true means every member carries a Medilink/Reservo id and the merge has to happen there first. truncated says the roster is larger than one scan.

Connected apps: refused with 403 CONNECTED_APP_SENSITIVE_DATA, whatever scopes they hold — this operation carries a dato sensible (ADR 0106 §4). Only an API key or a personal token of the workspace that holds the scope reaches it.

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

Example response (200)

{
  "data": {
    "groups": [],
    "scanned": 2,
    "truncated": false
  }
}

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

DELETE /clinic/relationships/{id}

Remove a family bond

Hard delete, deliberately: a relationship is a live PERMISSION, not a clinical fact. The record of who ACCESSED a ficha is clinic_record_event, which is append-only and untouched by this.

Connected apps: refused with 403 CONNECTED_APP_SENSITIVE_DATA, whatever scopes they hold — this operation carries a dato sensible (ADR 0106 §4). Only an API key or a personal token of the workspace that holds the scope reaches it.

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

Example response (200)

{
  "data": {
    "deleted": true
  }
}

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

PATCH /clinic/relationships/{id}

Edit one family bond

Partial update. Promoting a bond to is_primary demotes the incumbent first (the partial unique index allows exactly one per patient). Raising can_read_record needs clinic_record:write; lowering it never does — revoking access is always safe.

Connected apps: refused with 403 CONNECTED_APP_SENSITIVE_DATA, whatever scopes they hold — this operation carries a dato sensible (ADR 0106 §4). Only an API key or a personal token of the workspace that holds the scope reaches it.

ParameterInTypeRequiredConstraints
idpathuuidyes

Body

FieldTypeRequiredConstraints
related_patient_idstring | null—
roletitular \apoderado \tutor \
is_primaryboolean—
can_bookboolean—
can_read_recordboolean—
verified_methodphone_match \id_challenge \manual \
expires_atstring | null—date-time
notestring | null—máx. 500
curl -X PATCH https://api.vitrinadev.com/api/v1/clinic/relationships/<id> \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "can_read_record": false
  }'

Example response (200)

{
  "data": {
    "id": "6a73a912-0000-4000-8000-000000000002",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "clinic_patient_id": "b1b0b8de-0000-4000-8000-000000000001",
    "contact_id": "1a73af9e-0000-4000-8000-000000000002",
    "related_patient_id": null,
    "role": "apoderado",
    "is_primary": false,
    "can_book": true,
    "can_read_record": false,
    "verified_method": "id_challenge",
    "verified_at": "2026-09-22T22:21:17.284Z",
    "expires_at": null,
    "note": "Presentó cédula en el mesón",
    "created_by": null,
    "created_at": "2026-09-22T22:21:17.283Z",
    "updated_at": "2026-09-22T22:21:17.298Z",
    "contact_name": "Carmen Lagos Pinto",
    "contact_phone": "+56955554444",
    "patient_name": "María José Fuentes Lagos",
    "expired": false
  }
}

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

On this page