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.
Descarga la proyección completa de la API pública: openapi.json.
Identificar y fusionar pacientes explica este recurso en prosa, con ejemplos ejecutables.
| Método | Ruta | Qué hace |
|---|---|---|
POST | /clinic/auto-links/{id}/reject | «No es esta persona» — reject an automatic link for good |
GET | /clinic/contacts/{contactId}/auto-links | Patients automatically linked to a contact (Ads identity only) |
GET | /clinic/contacts/{contactId}/patient | The contact page's Paciente card |
GET | /clinic/contacts/{contactId}/payments | A contact's payment history |
GET | /clinic/contacts/{contactId}/payments/{paymentId}/boleta | A payment's boleta (PDF) |
GET | /clinic/contacts/{contactId}/relationships | Every patient one contact acts for |
GET | /clinic/patients | List clinic patients |
POST | /clinic/patients | Create 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}/attendance | A patient's attendance history |
GET | /clinic/patients/{id}/auto-links | Automatic contact links of a patient (Ads identity only) |
GET | /clinic/patients/{id}/citas | A patient's mirrored citas, newest first |
POST | /clinic/patients/{id}/merge | Merge a patient into another (tombstone, never a delete) |
GET | /clinic/patients/{id}/relationships | The family of one patient (apoderados, cargas, pareja) |
POST | /clinic/patients/{id}/relationships | Add somebody to a patient’s family |
GET | /clinic/patients/duplicates | Possible 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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|
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 '{}'Ejemplo de respuesta (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
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /clinic/contacts/{contactId}/auto-links
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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
contactId | path | uuid | sí |
curl https://api.vitrinadev.com/api/v1/clinic/contacts/<id>/auto-links \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (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"
}
]
}
}Responde: 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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
contactId | path | uuid | sí |
curl https://api.vitrinadev.com/api/v1/clinic/contacts/<id>/patient \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (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
}
]
}
}Responde: 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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
contactId | path | uuid | sí | |
limit | query | integer | no | ≥ 1, ≤ 200, por defecto 50 |
curl https://api.vitrinadev.com/api/v1/clinic/contacts/<id>/payments \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (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
}
}Responde: 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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
contactId | path | uuid | sí | |
paymentId | path | uuid | sí |
curl https://api.vitrinadev.com/api/v1/clinic/contacts/<id>/payments/<id>/boleta \
-H "Authorization: Bearer $VITRINA_KEY"Responde: 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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
contactId | path | uuid | sí |
curl https://api.vitrinadev.com/api/v1/clinic/contacts/<id>/relationships \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (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
}
]
}
}Responde: 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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
search | query | string | no | mín. 2, máx. 120 |
sort | query | nombre \ | rut \ | phone \ |
dir | query | asc \ | desc | 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/patients \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (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
}
}Responde: 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.
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
nombre | string | null | — | máx. 160 |
apellidos | string | null | — | máx. 160 |
rut | string | null | — | máx. 20 |
email | string | null | — | máx. 160 |
phone | string | null | — | máx. 40 |
birthdate | string | null | — | patrón ^\d{4}-\d{2}-\d{2}$ |
nombre_social | string | null | — | máx. 160 |
prevision | string | null | — | máx. 120 |
sexo | string | null | — | máx. 40 |
numero_ficha | string | null | — | máx. 60 |
ciudad | string | null | — | máx. 120 |
region | string | null | — | máx. 120 |
comuna | string | null | — | máx. 120 |
direccion | string | null | — | máx. 240 |
description | string | 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ú"
}'Ejemplo de respuesta (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"
}
}Responde: 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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
curl -X DELETE https://api.vitrinadev.com/api/v1/clinic/patients/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Responde: 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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
curl https://api.vitrinadev.com/api/v1/clinic/patients/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (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
}
}Responde: 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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
nombre | string | null | — | máx. 160 |
apellidos | string | null | — | máx. 160 |
rut | string | null | — | máx. 20 |
email | string | null | — | máx. 160 |
phone | string | null | — | máx. 40 |
birthdate | string | null | — | patrón ^\d{4}-\d{2}-\d{2}$ |
nombre_social | string | null | — | máx. 160 |
prevision | string | null | — | máx. 120 |
sexo | string | null | — | máx. 40 |
numero_ficha | string | null | — | máx. 60 |
ciudad | string | null | — | máx. 120 |
region | string | null | — | máx. 120 |
comuna | string | null | — | máx. 120 |
direccion | string | null | — | máx. 240 |
description | string | 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"
}'Ejemplo de respuesta (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"
}
}Responde: 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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí | |
late_cancel_hours | query | integer | no | ≥ 1, ≤ 168 |
limit | query | integer | no | ≥ 1, ≤ 50, por defecto 10 |
curl https://api.vitrinadev.com/api/v1/clinic/patients/<id>/attendance \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (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"
]
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /clinic/patients/{id}/auto-links
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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
curl https://api.vitrinadev.com/api/v1/clinic/patients/<id>/auto-links \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (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"
}
]
}
}Responde: 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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí | |
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/patients/<id>/citas \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (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
}
}Responde: 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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
into_patient_id | uuid | sí | |
reason | string | — | 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"
}'Ejemplo de respuesta (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": []
}
}Responde: 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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
curl https://api.vitrinadev.com/api/v1/clinic/patients/<id>/relationships \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (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
}
]
}
}Responde: 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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
contact_id | uuid | sí | |
related_patient_id | string | null | — | |
role | titular \ | apoderado \ | tutor \ |
is_primary | boolean | — | |
can_book | boolean | — | |
can_read_record | boolean | — | |
verified_method | phone_match \ | id_challenge \ | manual \ |
expires_at | string | null | — | date-time |
note | string | 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"
}'Ejemplo de respuesta (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
}
}Responde: 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"Ejemplo de respuesta (200)
{
"data": {
"groups": [],
"scanned": 2,
"truncated": false
}
}Responde: 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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
curl -X DELETE https://api.vitrinadev.com/api/v1/clinic/relationships/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"deleted": true
}
}Responde: 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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
related_patient_id | string | null | — | |
role | titular \ | apoderado \ | tutor \ |
is_primary | boolean | — | |
can_book | boolean | — | |
can_read_record | boolean | — | |
verified_method | phone_match \ | id_challenge \ | manual \ |
expires_at | string | null | — | date-time |
note | string | 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
}'Ejemplo de respuesta (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
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429