Record documents
Tests, reports and images hanging off a record, with their retention deadline — and the access and erasure requests of Chile's Ley 21.719. Marked sensitive.
Download the full API pública projection: openapi.json.
Add documents to a patient record explains this resource in prose, with runnable examples.
| Method | Path | What it does |
|---|---|---|
GET | /clinic/documents/{id}/content | Download one document’s bytes |
POST | /clinic/documents/{id}/delete | Delete a document before its retention runs out (tombstone) |
GET | /clinic/patients/{id}/documents | PHI documents on one patient’s ficha |
POST | /clinic/patients/{id}/documents | Upload a PHI document |
POST | /clinic/patients/{id}/erase | Erasure request (Ley 21.719), bounded by the retention floor |
POST | /clinic/patients/{id}/export | Export one patient’s whole clinical record |
GET | /clinic/privacy-requests | Export and erasure requests |
GET | /clinic/privacy-requests/{id}/download | Download an export archive |
GET /clinic/documents/{id}/content
Download one document’s bytes
Streams from the private bucket through this route rather than a signed URL. A purged document answers with the date it was disposed of and its reason — the record of it remains, the file does not. Every call writes a clinic_record_access_log row; a call that cannot write its access event fails.
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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
curl https://api.vitrinadev.com/api/v1/clinic/documents/<id>/content \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /clinic/documents/{id}/delete
Delete a document before its retention runs out (tombstone)
A file uploaded to the wrong ficha must be correctable, so this is allowed — but it writes the SAME tombstone the retention sweep writes, with the operator’s reason instead of «retention», so the two are equally visible in an audit. A POST because the reason is required. Every call writes a clinic_record_access_log row; a call that cannot write its access event fails.
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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
reason | string | yes | mín. 3, máx. 400 |
curl -X POST https://api.vitrinadev.com/api/v1/clinic/documents/<id>/delete \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"reason": "Cargado en la ficha equivocada"
}'Example response (200)
{
"data": {
"id": "16161616-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"clinic_patient_id": "12121212-0000-4000-8000-000000000001",
"encounter_id": "14141414-0000-4000-8000-000000000001",
"appointment_id": null,
"kind": "informe",
"title": "Informe de evaluación inicial",
"mime_type": "image/png",
"size_bytes": 95,
"sensitivity": "phi",
"uploaded_by_user_id": "11111111-0000-4000-8000-000000000001",
"uploaded_source": "staff",
"retention_until": "2041-09-24",
"purged_at": "2026-09-24T18:10:33.907Z",
"purge_reason": "Cargado en la ficha equivocada",
"created_at": "2026-09-24T13:44:10.552Z",
"updated_at": "2026-09-24T13:44:10.552Z"
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /clinic/patients/{id}/documents
PHI documents on one patient’s ficha
sensitivity defaults to phi because a file uploaded to a clinical record is health data until somebody says otherwise; the ficha marks those rows. Purged rows stay visible as tombstones — "we held an exam and disposed of it in 2042" is information. Requires clinic_record:read AND the per-patient PHI gate. Every call writes a clinic_record_access_log row; a call that cannot write its access event fails.
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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
kind | query | examen \ | imagen \ | informe \ |
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>/documents \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": {
"data": [],
"total": 0,
"page": 1,
"limit": 50
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /clinic/patients/{id}/documents
Upload a PHI document
THE TTL SHIPS WITH THE COLLECTION (ADR 0083 §5): retention_until is stamped from the tenant’s policy for this kind at upload time, and there is no code path that stores a document without one. Every call writes a clinic_record_access_log row; a call that cannot write its access event fails.
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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
kind | examen \ | imagen \ | informe \ |
title | string | null | — | máx. 300 |
content | string | yes | mín. 8, máx. 28000000 |
mime_type | string | null | — | máx. 160 |
sensitivity | phi \ | administrative | — |
encounter_id | string | null | — | |
appointment_id | string | null | — |
curl -X POST https://api.vitrinadev.com/api/v1/clinic/patients/<id>/documents \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"kind": "informe",
"title": "Informe de evaluación inicial",
"sensitivity": "phi",
"encounter_id": "14141414-0000-4000-8000-000000000001",
"mime_type": "image/png",
"content": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
}'Example response (201)
{
"data": {
"id": "16161616-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"clinic_patient_id": "12121212-0000-4000-8000-000000000001",
"encounter_id": "14141414-0000-4000-8000-000000000001",
"appointment_id": null,
"kind": "informe",
"title": "Informe de evaluación inicial",
"mime_type": "image/png",
"size_bytes": 95,
"sensitivity": "phi",
"uploaded_by_user_id": "11111111-0000-4000-8000-000000000001",
"uploaded_source": "staff",
"retention_until": "2041-09-24",
"purged_at": null,
"purge_reason": null,
"created_at": "2026-09-24T13:44:10.552Z",
"updated_at": "2026-09-24T13:44:10.552Z"
}
}Answers: 201 · 400 · 401 · 403 · 404 · 409 · 429
POST /clinic/patients/{id}/erase
Erasure request (Ley 21.719), bounded by the retention floor
INSIDE the 15-year window (Ley 20.584, Decreto 41) the request is REFUSED — with the legal reason and the date it may be executed — and the refusal is recorded; a silent refusal is as bad as a wrongful deletion. OUTSIDE it, the PII is tombstoned and the clinical rows stay for their own retention. A refusal answers 201, not a 4xx: the request was accepted and lawfully answered, and the row is the answer. Every call writes a clinic_record_access_log row; a call that cannot write its access event fails.
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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
reason | string | null | — | máx. 600 |
curl -X POST https://api.vitrinadev.com/api/v1/clinic/patients/<id>/erase \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"reason": "La paciente ejerce su derecho de supresión (Ley 21.719)"
}'Example response (201)
{
"data": {
"id": "17171717-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"clinic_patient_id": "12121212-0000-4000-8000-000000000001",
"kind": "export",
"status": "completed",
"requested_by_user_id": "11111111-0000-4000-8000-000000000001",
"requested_reason": "La paciente ejerce su derecho de acceso (Ley 21.719)",
"started_at": "2026-09-24T18:20:04.001Z",
"completed_at": "2026-09-24T18:20:07.430Z",
"size_bytes": 148223,
"token_expires_at": "2026-10-01T18:20:07.430Z",
"result": {
"documents": 3,
"photos": 1,
"notes": 6,
"consents": 2
},
"retention_until": "2026-10-01",
"refusal_reason": null,
"error": null,
"created_at": "2026-09-24T18:20:04.001Z",
"updated_at": "2026-09-24T18:20:07.430Z"
}
}Answers: 201 · 400 · 401 · 403 · 404 · 409 · 429
POST /clinic/patients/{id}/export
Export one patient’s whole clinical record
Assembles the ficha, its fichas filled, consents, clinical images and documents — with the documents’ own bytes inline — into one JSON archive, stores it, and returns a download path with a 7-day token. JSON and not a bundle of PDFs because Ley 21.719’s portability right is about the DATA; a rendered PDF is a picture of the data. Recorded as export in the PHI access log. Every call writes a clinic_record_access_log row; a call that cannot write its access event fails.
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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
reason | string | null | — | máx. 600 |
curl -X POST https://api.vitrinadev.com/api/v1/clinic/patients/<id>/export \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"reason": "La paciente ejerce su derecho de acceso (Ley 21.719)"
}'Example response (201)
{
"data": {
"id": "17171717-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"clinic_patient_id": "12121212-0000-4000-8000-000000000001",
"kind": "export",
"status": "completed",
"requested_by_user_id": "11111111-0000-4000-8000-000000000001",
"requested_reason": "La paciente ejerce su derecho de acceso (Ley 21.719)",
"started_at": "2026-09-24T18:20:04.001Z",
"completed_at": "2026-09-24T18:20:07.430Z",
"size_bytes": 148223,
"token_expires_at": "2026-10-01T18:20:07.430Z",
"result": {
"documents": 3,
"photos": 1,
"notes": 6,
"consents": 2
},
"retention_until": "2026-10-01",
"refusal_reason": null,
"error": null,
"created_at": "2026-09-24T18:20:04.001Z",
"updated_at": "2026-09-24T18:20:07.430Z"
}
}Answers: 201 · 400 · 401 · 403 · 404 · 409 · 429
GET /clinic/privacy-requests
Export and erasure requests
The durable record of who asked for what, when, and what came back — including a refused erasure and the date its window closes. Requires clinic_record: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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
clinic_patient_id | query | uuid | no | |
kind | query | export \ | erasure | no |
limit | query | integer | no | ≥ 1, ≤ 500, por defecto 100 |
curl https://api.vitrinadev.com/api/v1/clinic/privacy-requests \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": {
"data": [
{
"id": "1fa19e08-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"clinic_patient_id": "b1b0b8de-0000-4000-8000-000000000001",
"kind": "export",
"status": "failed",
"requested_by_user_id": "11111111-0000-4000-8000-000000000001",
"requested_reason": null,
"started_at": "2026-09-22T22:21:18.312Z",
"completed_at": "2026-09-22T22:21:18.499Z",
"storage_key": null,
"size_bytes": null,
"token_expires_at": null,
"result": {},
"retention_until": null,
"refusal_reason": null,
"error": "getaddrinfo ENOTFOUND minio",
"created_at": "2026-09-22T22:21:18.308Z",
"updated_at": "2026-09-22T22:21:18.498Z"
}
],
"total": 1
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /clinic/privacy-requests/{id}/download
Download an export archive
The token AND the staff gate are both checked: the token stops a stale link working forever, the gate stops the wrong staff member opening it. The archive is deleted when its token expires — PHI in a bucket must not outlive its link. Every call writes a clinic_record_access_log row; a call that cannot write its access event fails.
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.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
token | query | string | yes | mín. 10, máx. 4096 |
curl https://api.vitrinadev.com/api/v1/clinic/privacy-requests/<id>/download \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429