VitrinaAPI

Find and merge patient records

Read and merge identity records, and see who answers for a patient.

ClinicsOnly in clinic workspaces.

GET /clinic/patients lists the people the clinic treats, with their identity, their file number and who may book or read on their behalf. What was done to that person lives in Clinical record. Knowing that somebody is a patient of a clinic is already health data, which is why this whole chapter is marked sensitive.

This chapter asks for a scope of its own

Everything here demands clinic_patients:read, or clinic_patients:write to write, on top of the clinic scope. clinic:read isn't enough, even though it's the everyday scope the agenda is read with: it answers 403 FORBIDDEN naming the one that's missing. And no connected app reaches this, whatever scopes it holds: for it the answer is 403 CONNECTED_APP_SENSITIVE_DATA (Personal and health data).

Reading the register

curl "https://api.vitrinadev.com/api/v1/clinic/patients?limit=2" \
  -H "Authorization: Bearer $VITRINA_KEY"
{
  "data": {
    "data": [
      {
        "id": "12121212-0000-4000-8000-000000000001",
        "source": "native",
        "external_id": null,
        "rut": "12.345.678-5",
        "nombre": "María José",
        "apellidos": "Fuentes Lagos",
        "email": "[email protected]",
        "phone": "+56987654321",
        "birthdate": null,
        "nombre_social": null,
        "prevision": null,
        "numero_ficha": "44110",
        "direccion": "Av. Siempre Viva 742",
        "enabled": true,
        "contact_id": "22222222-0000-4000-8000-000000000001",
        "created_at": "2026-09-22T18:08:29.046Z",
        "updated_at": "2026-09-22T18:08:29.046Z"
      }
    ],
    "total": 1,
    "page": 1,
    "limit": 2
  }
}

Search with q, which covers name, RUT and file number. Filter by enabled and page with page and limit.

What's worth knowing about each field:

  • source says where the record comes from. native is a Vitrina record; healthatom or reservo, one that lives in the clinic's system and that Vitrina mirrors. external_id is what that system calls it.
  • nombre_social is the name the person asks to be called by, and it's the one to show when it's there.
  • numero_ficha is the number the front desk looks them up by. It isn't the id, and it isn't unique across systems.
  • contact_id is the person the clinic talks to over WhatsApp or email. A patient with no contact is a record nobody can reach yet.

Creating and editing

curl -X POST https://api.vitrinadev.com/api/v1/clinic/patients \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: alta-mjfuentes-2026-09-22" \
  -d '{
    "nombre": "María José",
    "apellidos": "Fuentes Lagos",
    "rut": "12.345.678-5",
    "phone": "+56987654321",
    "email": "[email protected]"
  }'

POST /clinic/patients creates a native record. A mirrored record is created in the system that owns it. If the clinic runs Medilink or Reservo, creating one here would invent a second person that system has never heard of. PATCH /clinic/patients/{id} edits, and DELETE only removes a native record with no history hanging off it.

Always send an Idempotency-Key on create. Without one, a retry after a dropped connection doesn't give you the same record back: it creates another person (Errors).

Duplicates and merging

GET /clinic/patients/duplicates groups the records that look like the same person and says why they look alike: same RUT, same phone, name and date of birth. Whoever decides sees the reason before the verdict.

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

Merging is POST /clinic/patients/{id}/merge, and it's never a delete. The absorbed record stays as a tombstone pointing at the survivor, so an id your system already stored still resolves. History, appointments and documents move to the winning record in one transaction.

Who answers for a patient

A patient doesn't always represent themselves: a minor has a guardian, a dependant has a policy holder, somebody books for their partner. That bond is explicit and carries permissions of its own:

{
  "data": {
    "data": [
      {
        "id": "33333333-0000-4000-8000-000000000001",
        "clinic_patient_id": "12121212-0000-4000-8000-000000000001",
        "contact_id": "22222222-0000-4000-8000-000000000001",
        "related_patient_id": null,
        "role": "titular",
        "is_primary": true,
        "can_book": true,
        "can_read_record": false,
        "verified_method": "manual",
        "verified_at": "2026-09-22T18:08:29.053Z",
        "expires_at": null,
        "expired": false
      }
    ]
  }
}

can_book and can_read_record are two different permissions: a mother can book for her sixteen-year-old without reading his record. expires_at closes the bond on its own, when the child comes of age, and expired states it already worked out so nobody has to compare dates.

The routes are GET|POST /clinic/patients/{id}/relationships and PATCH|DELETE /clinic/relationships/{id}. From the other side, GET /clinic/contacts/{contactId}/relationships lists every patient one person answers for, and GET /clinic/contacts/{contactId}/patient is the "Paciente" card on the contact page.

What gets recorded

Every read in this chapter writes a line into the record's access log, in the same transaction as the response. If the log can't be written, the read fails. The line says who read, which route they used and on which record, whether that was an API key or the person behind a personal token.

That log is how the clinic answers a patient about what was done with their data. It's read at GET /clinic/patients/{id}/record-events, which asks for clinic_admin:write on top of the record scope: who looked at a record is reviewed by the clinic's administration.

Events

EventWhen
clinic_patient.created · .updatedA record was created or edited
clinic_patient.deletedA native record with no history was removed
clinic_patient.mergedTwo records were the same person; carries the survivor

All of them arrive as a notice: they carry the id and data_omitted: "sensitive", never the body, even when the subscription asks for the resource's data. Reading the record means calling the API with a credential that holds the scope. That call goes through the access log (Webhooks).

What is not published

POST /clinic/patients/{id}/promote and the two link routes are the HealthAtom bond and aren't part of the public API.

On this page